Skip to content

refactor: app mode — customizable layout#11317

Open
eliheuer wants to merge 107 commits intoComfy-Org:mainfrom
eliheuer:app-mode-semi-customizable-layout
Open

refactor: app mode — customizable layout#11317
eliheuer wants to merge 107 commits intoComfy-Org:mainfrom
eliheuer:app-mode-semi-customizable-layout

Conversation

@eliheuer
Copy link
Copy Markdown

@eliheuer eliheuer commented Apr 17, 2026

Summary

Unifies App Mode and App Builder into a single WYSIWYG surface backed by
one shared store and one set of chrome components. Replaces the prior
parallel UIs based on user feedback and design discussions with Pablo,
Alex, Alex, and Christian.

Architecture rationale and alternatives in
ADR 0009.

What's in it

  • Shared chrome and store. AppChrome and FloatingPanel render in
    both modes; appModeStore is the single source of truth for panel
    preset, panel rows, viewport pan/zoom, and selected inputs/outputs.
    Builder edits propagate to App Mode without a translation layer.
  • Floating input panel. 6 preset positions (left/right dock, 4 float
    corners), drag-to-snap with a ghosted preview, drag-resizable in grid
    units, collapses to a header pill. Editable only in builder mode.
  • Multi-window output workspace. Each run's outputs render in their
    own movable OutputWindow inside a pan/zoom workspace — like a mood
    board. Replaces the single-output thumbnail strip. Run/Stop/progress
    live inside the active output window.
  • Builder simplifications. Input/output selection step dims the
    graph and rings selectable widgets/output nodes; the footer is now
    Exit / View app / Back / Next / Save with the 3-step progression as
    numbered badges.

Why one PR

The chrome, panel system, store, and output workspace interlock — splitting
the change would require flag-gated half-states or near-immediate rewrites
of one mode by the next PR. The PR is intentionally minimized to a single
round of unification: refactor only, no feature additions on top.

Test plan

  • pnpm typecheck
  • pnpm typecheck:browser
  • pnpm knip
  • pnpm lint
  • pnpm format
  • pnpm test:unit (40 tests in appModeStore.test.ts, 13 in
    outputWindowStore.test.ts, plus the existing suite)
  • pnpm test:browser browser_tests/tests/builderSaveFlow.spec.ts
    (will run in CI; not run locally)
  • Verify Run/Stop button visuals against the default Dark and Light
    palettes (saturated buttons use new --success-foreground /
    --destructive-foreground / --primary-foreground tokens via
    text-(--token) arbitrary-value syntax).

Known follow-ups (not in this PR)

  • Four cell components (InputCell, ModeToggleCell, FeedbackCell,
    PanelBlockList) reset child-component chrome via :deep() +
    !important. Each !important has an inline TODO citing a
    layout prop on NodeWidgets as the proper upstream fix.
  • Three new *-foreground semantic tokens are referenced via
    arbitrary-value syntax in App Mode but not exposed as Tailwind
    utility classes. Promoting them and backfilling the existing
    dormant misuses is documented as
    PR #11 in comfyui-theme-tools's
    upstream-pr-opportunities doc.
  • Theme-system cascade gaps (--contrast-mix-color undefined in
    .dark-theme, --interface-panel-surface shadowed in style.css)
    are real but pre-existing on main. Documented in the same
    follow-up doc.

Screenshots

Screenshot 2026-04-27 at 8 27 00 AM (2) Screenshot 2026-04-27 at 2 00 23 PM (2) Screenshot 2026-04-28 at 5 07 02 PM (2) Screenshot 2026-04-29 at 9 16 07 AM (2) Screenshot 2026-05-01 at 10 40 07 AM (2)
2026-05-01.14-20-08.mp4

┆Issue is synchronized with this Notion page by Unito

eliheuer added 10 commits April 16, 2026 14:42
Introduces LayoutGrid.vue primitive with cell sizing, viewport
measurement, and gap expansion. Adds system cells: mode toggle,
feedback, run, batch-count. Wires real workflow execution via
Comfy.QueuePrompt with run metadata and error logging.

Squashes ~22 exploratory commits from the Phase 1 build-out,
including the feat+revert pair that introduced and then removed
the initial CSS-grid-per-widget approach.
Each selected input (prompt, width, height, lora_strength, steps, etc.)
now renders in its own bento cell instead of the one-big-column
InputsCell wrapper.

- New InputCell.vue renders a single widget by mirroring
  AppModeWidgetList's per-widget block exactly: header row with label,
  DropZone + NodeWidgets with the specific Tailwind classes
  ('**:[.col-span-2]:grid-cols-1' etc.) that force full-width widget
  layout. provide(HideLayoutFieldKey, true) hides each widget's
  internal label so we show one cell-level label per cell.
- BentoView computes per-widget entries by replicating the filtered-
  nodeData pattern from AppModeWidgetList, then generates one
  BentoCellPlacement per entry with auto-layout: INPUT_COL=-11,
  INPUT_COL_SPAN=10, rowSpan=4 for multiline widgets (textareas,
  customtext) / rowSpan=2 for everything else, stacked top-down.
- Stored per-widget layout config (colSpan/col/row) in linearData is
  intentionally ignored for now; earlier width-picker experiments
  wrote values that fight the heuristic. Phase 3 reintroduces
  stored-config support once drag/resize is wired.
- InputsCell.vue removed.
- Outputs hero narrowed from colSpan 14 to 10 so it sits cleanly
  beside the wider input column.

All 4+ input cells render at the same width and same widget-rendering
path as the old UI. Tested end-to-end with the qwen_image_illustration
_lora template via 'pnpm dev:cloud'.
Phase 2b: when a workflow has multiple selected outputs, each extra
output renders in its own bento cell.

- OutputCell.vue: single-output preview driven by
  linearOutputStore.inProgressItems filtered to a specific nodeId.
  Renders MediaOutputPreview or an empty-state message.
- BentoView: first selected output keeps the hero cell (wraps
  LinearPreview — preserves history, progress, latent previews,
  welcome state). Additional outputs stack as OutputCells in a
  3-col column at col 11. Hero narrows to colSpan 7 when extras
  exist.
- outputNodeMap exposes the nodeId for each output cell to its
  template slot.

Also adds the App-Mode design-token system:
- design-tokens.css defines the 4-size type scale (16/18/22/48),
  color palette, grid constants, and motion tokens as CSS custom
  properties scoped to .bento-view
- README.md documents conventions: read tokens before editing,
  tight scale, no decorative chrome, token references over hex
  literals
- All bento cells (BentoGrid, InputCell, OutputCell, IconCell,
  RunCell, BatchCountCell, FeedbackCell) migrated to var(--bento-*)
  — no hardcoded colors, font sizes, or durations

Font scale unified at 16px for widget content (was 12px) and all
labels — reads significantly better in bento cells and normalizes
the previously-scattered 12/13/14/16/20/22/48px set.
Floating panel shell over the output canvas with drag-to-snap
behavior across 6 presets (right/left dock, float tr/br/tl/bl)
and a Comfy-brand-blue snap preview. Panel interior uses
Notion-style blocks that reorder inline and support multi-column
rows via side-drag. Drops orphaned output cells and unexports
internal panel options.
- Relocate LinearPreview chrome into the layout grid
- Bump chrome icons to 20px, unify thumb active color
- Panel spacing, layout, and typography pass
- Grid-aware panel presets with content-driven block heights
- Full-height grip; Comfy brand blue for drop cues
Mechanical rename of the bento/ directory and related symbols
to layout/, plus README rewrite and phase-map cleanup.
No behavior change.
- Replace welcome heading with Comfy-style SVG wordmark
- Offset welcome for the right-dock panel; v-center the block
- Left-align body copy at 3xl
- Unify the welcome Run pill with the corner Run button
- Share SideToolbar with Graph mode (App Mode previously had a
  custom subset)
- Panel-matched chrome across mode toggle, builder, share,
  feedback, and action cells; real Run pill in welcome
- Decorative dot grid on the canvas background
- Simplify info cell; round output thumbnails
- Move output history thumbs from column 1 to row 1 so column 1 is
  free for the left-dock panel to sit flush against the SideToolbar.
- Drop the column-1 clearance from .floating-panel--left-dock /
  --float-tl / --float-bl left offsets (and matching drag-preview
  positions); left edge now mirrors right-dock at the outer padding.
- RunCell: route queue-prompt failures through toastErrorHandler
  instead of console.error so silent failures surface to the user.
…h t()

Adds two keys under linearMode:
- outputThumbAlt (fallback for asset.name in OutputThumbCell)
- blockDragReorderAria (with {kind} interpolation in PanelBlockList)
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

@eliheuer is attempting to deploy a commit to the ComfyUI Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces App Mode workspace with full-viewport pan/zoom, floating output windows with tiling/relayout, a dockable/floating panel system with drag/resize/reorder, selection chrome overlays, preview mode adjustments (hide chrome, hideInfo), builder backdrop/panel and toolbar/menu/footer refactors, schema/store expansions for layout persistence, theme/i18n tokens, utilities, and updated tests/browser flows.

Changes

App Mode and Builder

Layer / File(s) Summary
Data & Validation
src/platform/workflow/..., src/types/widgetTypes.ts, src/locales/en/main.json
LinearData adds optional layout and per-input grid fields; Zod schema adds panel-preset enum and layout passthrough; new HideInputSelectionKey injection key; i18n keys for App Mode UI.
Stores & Sync
src/stores/appModeStore.ts, src/renderer/extensions/linearMode/outputWindowStore.ts, src/renderer/extensions/linearMode/useOutputWindowSync.ts
Adds viewport/panel state and controls (zoom/pan/noZoom), persisted panel layout, selection helpers; new outputWindowPinia store with spawn, tiling, relayout, pruning, promote; sync composable bridges execution items → windows and attaches assets.
Panel System / Composables
src/components/appMode/layout/panels/*
Panel preset classes/types, PanelHeader, FloatingPanel, PanelDragPreview, usePointerDrag/usePanelDrag/usePanelResize, block reorder (useBlockDrag), FLIP reorder helper, and panel-layout reconciliation (useAppPanelLayout + applyMove).
Layout & Windows
src/components/appMode/layout/LayoutView.vue, AppChrome.vue, OutputWindow*.vue, OutputWindowList.vue
LayoutView implements workspace transform, mirrored dot-grid, reacts to new windows for flyTo/relayout; AppChrome provides corner rails/cells; OutputWindow + OutputWindowList implement draggable/resizable tiles with previews, progress/ETA overlay, rerun/download actions.
Previews & Selection
src/renderer/extensions/linearMode/LinearPreview.vue, MediaOutputPreview.vue, ImagePreview.vue, VideoPreview.vue, SelectionChrome.vue
Hide-chrome mode renders OutputWindowList and progress/ETA; media previews accept hideInfo and fit; LatentPreview uses placeholder variants; SelectionChrome teleports selection overlay for inputs/outputs.
Builder UI
src/components/builder/*, BuilderPanel.vue, BuilderBackdrop.vue
Selection overlays via SelectionChrome, BuilderBackdrop for arrange (wheel/pan), right-docked BuilderPanel using FloatingPanel + PanelBlockList, toolbar/menu/footer refactors, IoItem removed.
Views / Wiring
src/views/GraphView.vue, src/views/LinearView.vue, LiteGraphCanvasSplitterOverlay.vue
Keeps canvas mounted in builder, adds conditional LayoutView for App Mode, select-mode scrim, and narrows offside splitter activation condition.
Styling & Theme
packages/design-system/src/css/style.css, src/assets/css/style.css
Adds layout design tokens, --panel-dock-width, .panel-chrome surface, app-mode widget theming, radius/typography tokens, and minor reduced-motion comment cleanup.
Utilities
src/utils/nodeTitleUtil.ts, src/utils/widgetSubtitleUtil.ts
Adds friendlyNodeLabel with SHORT_NODE_LABELS; widgetSubtitle with type/name-pattern overrides and tests.
Tests & Browser
unit + e2e files
Adds/updates tests: workflow schema (grid/layout), outputWindowStore tests, appModeStore viewport tests, widgetSubtitle tests; browser tests updated to open Save As via top-left menu and helpers/selectors pruned.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LinearPreview
  participant OutputWindowStore
  participant AppModeStore
  participant OutputWindowList
  User->>LinearPreview: Run workflow
  LinearPreview->>OutputWindowStore: useOutputWindowSync() upsert
  AppModeStore-->>OutputWindowStore: noZoom/relayout or flyTo
  OutputWindowStore-->>OutputWindowList: windows (sorted)
  User->>OutputWindowList: drag/resize/promote/download/rerun
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A canvas of dots where rabbits zoom,
Panels hop-dock, with gentle whomp-boom;
Windows float light as dandelion seeds,
Builder burrows, arranging feeds.
Click—rerun!—the carrot glows bright;
Ship it, we thump in delight. 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

🎨 Storybook: loading Building...

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

🎭 Playwright: ⏳ Running...

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (19)
src/renderer/extensions/linearMode/LinearWelcome.test.ts (1)

40-44: Add behavioral assertions for the new run-command path.

This mock is wired, but the suite still doesn’t verify that clicking Run calls Comfy.QueuePrompt and shift-click calls Comfy.QueuePromptFront. Please add those two assertions so the new dispatch behavior is protected.

As per coding guidelines: “Aim for behavioral coverage of critical and new features in tests.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/extensions/linearMode/LinearWelcome.test.ts` around lines 40 -
44, The test currently mocks useCommandStore's execute but doesn't assert the
new behaviors; update LinearWelcome.test.ts to assert that clicking the "Run"
button calls the command store's execute with "Comfy.QueuePrompt" and that
shift-clicking the same button calls it with "Comfy.QueuePromptFront". Locate
the mocked useCommandStore (vi.mock('@/stores/commandStore') returning execute:
vi.fn()), simulate a normal click and a click with shiftKey (e.g., fireEvent or
userEvent with { shiftKey: true }), then add expectations asserting execute was
called with those exact command names (and appropriate payload if applicable);
also ensure you clear or reset the mock between interactions so calls are
isolated.
src/components/appMode/layout/cells/ModeToggleCell.vue (1)

22-47: Avoid deep !important overrides for the dropdown shell.

Line 35-Line 39 relies on deep internal class targeting plus !important, which is fragile and hard to maintain. Prefer exposing a layout-specific variant/class hook from WorkflowActionsDropdown so this cell can style it without scoped CSS overrides.

As per coding guidelines: “Avoid <style> blocks in Vue components; use Tailwind 4 utility classes for styling” and “Do not use !important or the ! important prefix for Tailwind classes.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/ModeToggleCell.vue` around lines 22 - 47,
The scoped CSS in ModeToggleCell.vue is using deep selectors and !important to
override WorkflowActionsDropdown internals; instead, add a layout-specific
prop/class hook to WorkflowActionsDropdown (e.g., a variant or className prop)
that applies the transparent background, zero padding and full-width Tailwind
utilities from the dropdown root, then use that prop when rendering
<WorkflowActionsDropdown> inside ModeToggleCell and remove the deep :deep(...)
rules and !important overrides; also replace remaining styling with Tailwind
utility classes on the ModeToggleCell root and the new exposed dropdown variant
so no scoped style block or !important usage is required.
src/renderer/extensions/linearMode/LinearPreview.vue (1)

158-161: Nit: duplicated word in comment ("layout / layout").

The comment reads "even when the layout / layout hides this bar". Drop the duplicate so it reads "even when the layout hides this bar".

✏️ Proposed fix
-  <!-- The inner OutputHistory must stay mounted even when the layout
-       layout hides this bar — its watchers drive selectedItem /
-       selectedOutput via `updateSelection`. v-show keeps the DOM +
-       watchers alive; display:none just hides it visually. -->
+  <!-- The inner OutputHistory must stay mounted even when the layout
+       hides this bar — its watchers drive selectedItem / selectedOutput
+       via `updateSelection`. v-show keeps the DOM + watchers alive. -->
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/extensions/linearMode/LinearPreview.vue` around lines 158 - 161,
The comment above the OutputHistory element contains a duplicated word "layout /
layout"; edit the comment in LinearPreview.vue so it reads "even when the layout
hides this bar" (keep the rest of the comment intact), ensuring references to
OutputHistory, v-show, updateSelection, selectedItem/selectedOutput remain
unchanged.
src/components/appMode/layout/cells/IconCell.vue (1)

17-19: Prefer emit over a callback prop for activate.

Per guideline "src/components/**/*.vue: Prefer emit/@event-name for state changes in Vue components". The onActivate prop-callback pattern works (Vue forwards @activate listeners into the onActivate attr), but using defineEmits<{ activate: [] }>() + emit('activate') is the idiomatic choice and matches the call sites in LayoutView.vue (@activate="..."). Based on coding guidelines: "Prefer emit/@event-name for state changes in Vue components".

♻️ Proposed refactor
-defineProps<{
-  /** Lucide / comfy icon class, e.g. 'icon-[lucide--hammer]' */
-  icon: string
-  /** Accessible label and tooltip text (already-translated string) */
-  label: string
-  /** Whether the cell shows an active/selected visual state */
-  active?: boolean
-  /** Whether the cell is disabled */
-  disabled?: boolean
-  /** Click handler */
-  onActivate?: () => void | Promise<void>
-}>()
+defineProps<{
+  icon: string
+  label: string
+  active?: boolean
+  disabled?: boolean
+}>()
+const emit = defineEmits<{ activate: [] }>()
-    `@click`="() => onActivate?.()"
+    `@click`="emit('activate')"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/IconCell.vue` around lines 17 - 19,
Replace the onActivate prop-callback pattern in IconCell.vue with Vue emits:
remove the onActivate prop from the props/type declaration, add defineEmits<{
activate: [] }>() and call emit('activate') where onActivate() was previously
invoked; update any TypeScript types if needed and ensure consumers
(LayoutView.vue) keep using `@activate`="..." (no changes required there) so the
component uses the idiomatic emit('activate') event instead of a callback prop.
src/components/appMode/layout/cells/RunCell.vue (2)

17-19: Simplify the event typing.

@click supplies a MouseEvent, so the 'shiftKey' in e narrowing and the as KeyboardEvent | MouseEvent cast are unnecessary.

♻️ Proposed simplification
-async function handleClick(e: Event) {
-  const priority = 'shiftKey' in e && (e as KeyboardEvent | MouseEvent).shiftKey
-  const commandId = priority ? 'Comfy.QueuePromptFront' : 'Comfy.QueuePrompt'
+async function handleClick(e: MouseEvent) {
+  const commandId = e.shiftKey ? 'Comfy.QueuePromptFront' : 'Comfy.QueuePrompt'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/RunCell.vue` around lines 17 - 19, The
handleClick function is over-narrowing the event; change its signature to accept
a MouseEvent (async function handleClick(e: MouseEvent)) and remove the
`'shiftKey' in e` check and the cast—compute priority directly with e.shiftKey
and keep the commandId logic (const priority = e.shiftKey; const commandId =
priority ? 'Comfy.QueuePromptFront' : 'Comfy.QueuePrompt'). This simplifies
typing and removes the unnecessary KeyboardEvent | MouseEvent cast while keeping
the behavior intact.

34-42: Consider using the shared Button component.

The repo convention (see learnings / src/components/ui/button/Button.vue) is to use the shared Button rather than raw <button>. If this cell's accent styling doesn't map to an existing variant it may be acceptable to keep the raw button, but consider introducing or reusing a variant so the design-token-driven accent isn't re-implemented per-cell as the layout grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/RunCell.vue` around lines 34 - 42,
Replace the raw <button> in RunCell.vue with the shared Button component from
src/components/ui/button/Button.vue: import and register Button in the
component, use <Button> in place of the element and forward props/data
attributes (type="button", data-testid="layout-run-cell") and the click handler
(handleClick) as `@click`. Map the cell styling to an existing Button prop (e.g.,
variant="accent") and if no suitable variant exists, add a new variant to
Button.vue (styling/token) so the run-cell appearance comes from the shared
Button rather than a bespoke element; keep the icon and label in the Button
slot. Ensure component name references: RunCell.vue, handleClick, and
Button.vue.
src/components/appMode/layout/README.md (1)

1-35: Minor: consider whether a link to a private repo belongs in public source docs.

Line 3 points readers to comfyui-app-mode (private) and Line 29 references its todo.md. External contributors hitting 404s may find this confusing. Either drop the pointer or gate the phase map on what's actually public.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/README.md` around lines 1 - 35, The README
links to a private repo (`comfyui-app-mode`) and its `todo.md`, which will
confuse external contributors; update the README.md to either remove the direct
link to `comfyui-app-mode` and `todo.md`, or replace it with a clear note that
the referenced repo is private and provide a public summary of the phase map (or
extract the relevant `Phase map` contents into this README). Edit the README.md
header where `comfyui-app-mode` is mentioned and the Phase map paragraph to
either omit the private link or add a short public-facing summary and a “private
repo — contact owner” note so public docs don’t point to 404s.
src/components/appMode/layout/cells/BatchCountCell.vue (1)

18-21: Prefer $t in the template since t isn't used in script.

Minor cleanup: t is only referenced inside <template>, so you can drop useI18n / t from the script and use the built-in $t('linearMode.runCount') in the template.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/BatchCountCell.vue` around lines 18 - 21,
Remove the unused script-level i18n import and binding: delete the call to
useI18n() and the const { t } = useI18n() declaration in BatchCountCell.vue, and
update the template to call the global $t('linearMode.runCount') instead of
relying on the removed t; ensure no other script references to t remain and keep
storeToRefs(useQueueSettingsStore()) and useSettingStore() unchanged.
src/components/appMode/layout/panels/usePanelDrag.ts (3)

82-90: Reset pointer-capture state before calling onCommit.

If onCommit throws, activePointerId and capturedEl stay non-null even though the pointer has already been released, which would make the next drag's cleanup branch attempt to release a stale pointer id. Reset state first, then invoke the callback:

♻️ Proposed refactor
-    window.removeEventListener('pointermove', onPointerMove)
-    window.removeEventListener('pointerup', onPointerUp)
-    window.removeEventListener('pointercancel', onPointerUp)
-
-    if (snapTarget.value !== opts.currentPreset.value) {
-      opts.onCommit(snapTarget.value)
-    }
-    activePointerId = null
-    capturedEl = null
+    window.removeEventListener('pointermove', onPointerMove)
+    window.removeEventListener('pointerup', onPointerUp)
+    window.removeEventListener('pointercancel', onPointerUp)
+    activePointerId = null
+    capturedEl = null
+
+    if (snapTarget.value !== opts.currentPreset.value) {
+      opts.onCommit(snapTarget.value)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/usePanelDrag.ts` around lines 82 - 90,
The cleanup in onPointerUp should reset the pointer-capture state before calling
the commit callback to avoid leaving stale state if opts.onCommit throws; move
the assignments activePointerId = null and capturedEl = null (and keep removing
the window listeners) so they occur before the conditional
opts.onCommit(snapTarget.value), i.e. clear capturedEl/activePointerId first,
then call opts.onCommit when snapTarget differs from opts.currentPreset.

13-13: PANEL_HALF_WIDTH constant can desync from --panel-dock-width.

FloatingPanel sizes itself from var(--panel-dock-width, 420px) while this composable hardcodes 210 as the half-width for snap-anchor math. If the CSS variable (or FloatingPanel's default) changes — or is themed per user — snap anchors will land off-center.

Consider reading it once from computed style:

function panelHalfWidth(): number {
  const raw = getComputedStyle(document.documentElement)
    .getPropertyValue('--panel-dock-width')
    .trim()
  const px = parseFloat(raw) || 420
  return px / 2
}

…and recomputing inside presetAnchors. Low-priority for Phase 4-C but easy to wire so the two locations stay in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/usePanelDrag.ts` at line 13,
PANEL_HALF_WIDTH is a hardcoded constant that can drift from FloatingPanel's CSS
variable --panel-dock-width, causing preset anchor math in presetAnchors to
misalign; replace the constant with a small helper (e.g., panelHalfWidth()) that
reads
getComputedStyle(document.documentElement).getPropertyValue('--panel-dock-width'),
parses to px with a 420px fallback and returns half the value, and call that
helper inside presetAnchors (and any other places using PANEL_HALF_WIDTH) so
anchors recompute from the actual CSS value rather than a fixed 210.

115-119: onBeforeUnmount should fully tear down an in-flight drag.

If the component unmounts mid-drag, the current cleanup only removes window listeners — it leaves the captured pointer on capturedEl (never released) and skips the "commit preset on release" path. Call endDrag() so pointer capture is released and onCommit can still fire (or explicitly skip commit, your call), and state is reset:

♻️ Proposed refactor
 onBeforeUnmount(() => {
-  window.removeEventListener('pointermove', onPointerMove)
-  window.removeEventListener('pointerup', onPointerUp)
-  window.removeEventListener('pointercancel', onPointerUp)
+  if (isDragging.value) endDrag()
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/usePanelDrag.ts` around lines 115 - 119,
The onBeforeUnmount cleanup currently only removes window listeners and leaves
an in-flight drag state (capturedEl pointer capture never released and onCommit
not invoked); modify the onBeforeUnmount handler in usePanelDrag to call
endDrag() (or explicitly call the same teardown/commit logic used by endDrag)
before removing listeners so pointer capture on capturedEl is released, onCommit
runs (or the commit is skipped intentionally), and internal drag state is reset.
src/views/LinearView.vue (1)

126-132: Resize-disabled Splitter may be better expressed as flex.

The inner Splitter inside the showLayoutView branch has resize disabled (@resizestart="$event.originalEvent.preventDefault()") and uses fixed :size props for both panels, so the only thing the Splitter contributes over a plain flex layout is the gutter visual. Given this bypasses PrimeVue's sizing contract (no @resizeend / sizer, state-storage="local" persists widths that are never actually changeable), consider replacing it with a flex row + divider to avoid surprising users with a gutter that looks draggable but isn't.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/LinearView.vue` around lines 126 - 132, The Splitter used inside
the showLayoutView branch (component Splitter with
state-key="Comfy.LinearView.SidebarPanelSplitter", state-storage="local",
`@resizestart`="$event.originalEvent.preventDefault()", and fixed :size props) is
effectively non-resizable and persists widths that never change; replace it with
a simple flex row layout (use container classes like flex/row and two children
with the same fixed sizes or flex-basis) and render a non-interactive visual
divider/gutter instead of the PrimeVue Splitter so the UI no longer shows a
draggable gutter that does nothing while preserving the current panel widths and
appearance and removing state-storage and resizestart usage.
src/components/appMode/layout/cells/OutputThumbCell.vue (1)

50-64: Compute media type once.

getMediaType(output) is evaluated twice per render (once per v-if/v-else-if). Extract into a computed so the result is cached and the template reads more cleanly:

♻️ Proposed refactor
 const isActive = computed(() => selectedId.value === selectionId.value)
+const mediaType = computed(() => getMediaType(props.output))
-    <img
-      v-if="getMediaType(output) === 'images'"
+    <img
+      v-if="mediaType === 'images'"
       ...
-    <video
-      v-else-if="getMediaType(output) === 'video'"
+    <video
+      v-else-if="mediaType === 'video'"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/OutputThumbCell.vue` around lines 50 -
64, Compute getMediaType(output) once by creating a computed property (e.g.,
mediaType = computed(() => getMediaType(output))) in the OutputThumbCell
component and use that computed value in the template v-if/v-else-if instead of
calling getMediaType(output) twice; update the script block to import/use Vue's
computed (or add a computed option if using Options API) so mediaType updates
when output changes and replace both template calls with mediaType === 'images'
and mediaType === 'video'.
src/components/appMode/layout/cells/FeedbackCell.vue (1)

57-75: !important stack is fighting TypeformPopoverButton's variant="inverted".

These :deep(button|a) overrides use !important to undo the white background/color baked into variant="inverted" of TypeformPopoverButton. This is a design-system smell: the variant isn't appropriate for this surface.

Cleaner alternatives:

  • Add a muted/ghost variant on TypeformPopoverButton (or expose a variant prop pass-through) and use it here instead of overriding.
  • Drop variant="inverted" and rely on $attrs class forwarding so the caller can pass class="bg-transparent text-muted hover:bg-..." through tailwind-merge.

Either approach removes the !important escape hatch and the deep selectors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/FeedbackCell.vue` around lines 57 - 75,
The CSS in FeedbackCell.vue currently uses deep selectors with !important to
override TypeformPopoverButton's variant="inverted"; instead, add a muted/ghost
variant (or expose a variant prop pass-through) to TypeformPopoverButton and use
that variant in FeedbackCell, or remove variant="inverted" and rely on
$attrs/class forwarding so the caller can pass transparent/muted classes; then
delete the .feedback-cell :deep(button|a) rules and the !important overrides
(and the related hover and icon sizing deep selectors if unnecessary), ensuring
FeedbackCell uses the new variant or forwarded classes instead of style
overrides.
src/components/appMode/layout/panels/PanelDragPreview.vue (1)

45-88: Duplicated preset-positioning CSS with FloatingPanel.vue.

All six preset rules (right-dock, left-dock, float-{tr,br,tl,bl}) are copied verbatim from src/components/appMode/layout/panels/FloatingPanel.vue (lines 79-123). If FloatingPanel's positions ever drift (e.g., a corner's height/offset changes), the drag preview silently desyncs from the actual landing rect.

Consider extracting the shared positions into a reusable (unscoped) CSS class or CSS custom properties keyed by preset so both components resolve positions from a single source of truth, e.g.:

/* shared, e.g. layout/panels/panelPresets.css */
.panel-preset--right-dock { top: var(--layout-outer-padding); right: ... }

and have .floating-panel--right-dock / .panel-drag-preview--right-dock compose it via @apply or a shared selector.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/PanelDragPreview.vue` around lines 45 -
88, Duplicate preset positioning CSS in PanelDragPreview.vue (classes
.panel-drag-preview--right-dock, .panel-drag-preview--left-dock,
.panel-drag-preview--float-tr/br/tl/bl) should be extracted so it stays in sync
with FloatingPanel.vue; create a single, unscoped shared stylesheet (e.g.,
panelPresets.css) that defines .panel-preset--right-dock,
.panel-preset--left-dock and .panel-preset--float-tr/br/tl/bl using the same
vars, then update FloatingPanel.vue and PanelDragPreview.vue to compose or reuse
those shared classes/selectors (via `@apply`, multiple class names, or a shared
selector) instead of duplicating the six rules. Ensure the new shared file is
imported globally or into both components so the presets remain the single
source of truth.
src/components/appMode/layout/panels/FloatingPanel.vue (1)

29-32: Prefer an emit over a callback prop for pointerdown forwarding.

Coding guidelines prefer emit/@event-name for state changes over callback props. Swap onHeaderPointerDown for an emit:

-  onHeaderPointerDown?: (e: PointerEvent) => void
+// emit instead
+const emit = defineEmits<{ headerPointerDown: [e: PointerEvent] }>()
-    <header
-      class="floating-panel__header"
-      `@pointerdown`="onHeaderPointerDown?.($event)"
-    >
+    <header
+      class="floating-panel__header"
+      `@pointerdown`="emit('headerPointerDown', $event)"
+    >

…and in LayoutView.vue: @header-pointer-down="onHeaderPointerDown". As per coding guidelines: "Prefer emit/@event-name for state changes in Vue components".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/FloatingPanel.vue` around lines 29 - 32,
Replace the callback prop pattern: remove the onHeaderPointerDown prop from
FloatingPanel and instead emit a 'header-pointer-down' event from the header
handler (use $emit('header-pointer-down', $event) or defineEmits in script
setup); update FloatingPanel's template to use `@pointerdown` to emit rather than
calling the prop. Update any TypeScript/prop typings accordingly (remove prop
type and adjust emits type if present). Finally, update the parent (LayoutView)
to listen with `@header-pointer-down`="onHeaderPointerDown" (keeping its handler
name) so the parent behavior is preserved.
src/components/appMode/layout/cells/InputCell.vue (1)

96-106: !important overrides may hide a better fix upstream.

These :deep() rules force font-size/height/resize on widget inputs to undo text-xs / explicit heights coming from WidgetTextarea / WidgetInputNumber etc. Per guidelines, prefer fixing interfering styles at the source (e.g., conditional class in the widget based on HideLayoutFieldKey/injected layout context) rather than stacking !important. Acceptable short-term given Phase 2a scope, but worth a TODO so this doesn't calcify.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/InputCell.vue` around lines 96 - 106, The
current CSS in InputCell.vue uses :deep selectors with !important to override
widget styles, which masks the real fix; instead update the widget components
(WidgetTextarea, WidgetInputNumber) to respect the layout context by checking
the injected HideLayoutFieldKey or a provided layout flag and apply a
conditional class that disables their text-xs/explicit height/resizable styles
when rendered inside InputCell; remove the !important overrides from
InputCell.vue (or replace them with a simple non-!important selector) and add a
TODO comment referencing HideLayoutFieldKey and the conditional class approach
so downstream code uses the widget-level fix rather than forcing overrides here.
src/components/appMode/layout/panels/panelTypes.ts (1)

29-45: Drop the …Base indirection — these types aren’t extended anywhere.

No other type extends InputBlockBase/RunBlockBase, so the indirection just adds noise.

♻️ Proposed refactor
-interface InputBlockBase {
+export interface InputBlock {
   id: string
   kind: 'input'
   /** `${nodeId}:${widgetName}` — matches InputCellEntry.key */
   entryKey: string
   /** Sizing hint so the panel gives textareas room to breathe. */
   isMultiline?: boolean
 }
-export type InputBlock = InputBlockBase
 
-interface RunBlockBase {
+export interface RunBlock {
   id: string
   kind: 'run'
   /** Whether to show the batch-count row above the run button. */
   withBatchCount?: boolean
 }
-export type RunBlock = RunBlockBase
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/panelTypes.ts` around lines 29 - 45,
Remove the unnecessary "...Base" indirection by collapsing InputBlockBase and
RunBlockBase into their exported types: replace the separate interface
declarations InputBlockBase and RunBlockBase with direct exported
types/interfaces named InputBlock and RunBlock (keeping all existing fields: id,
kind, entryKey, isMultiline for InputBlock and id, kind, withBatchCount for
RunBlock), and update any other code that imports or references
InputBlockBase/RunBlockBase to use InputBlock/RunBlock instead of the Base
names; ensure JSDoc/comments (e.g., the entryKey comment) are preserved on the
final types.
src/components/appMode/layout/panels/PanelBlockList.vue (1)

143-285: Scoped <style> block + !important conflicts with repo styling guidelines.

Per guidelines, new components should use Tailwind 4 utilities instead of <style> blocks, and should avoid !important. A lot of what is here (flex/gap/padding/sizing) maps directly to Tailwind utilities; the bits that can't (the :deep(...) overrides of data-testid='node-widgets', the field-sizing: content declarations) are the ones that actually justify custom CSS. The !important declarations on lines 244–254, 276–285 look like they’re fighting the NodeWidgets grid — worth digging into whether the real fix is to propagate a prop/variant into NodeWidgets rather than clobbering it from the outside.

Not a blocker for the draft, but flagging so it doesn't land as-is. As per coding guidelines: "Avoid <style> blocks in Vue components; use Tailwind 4 utility classes for styling" and "Do not use !important or the ! important prefix".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/PanelBlockList.vue` around lines 143 -
285, The scoped <style> uses many rules that should be converted to Tailwind
utilities and it misuses !important; remove or migrate the bulk of layout rules
(panel-block-list, panel-block-row, panel-block, panel-block__grip,
panel-block__content, panel-block__run-batch, etc.) into Tailwind 4 classes on
the corresponding template elements, and eliminate all `!important` usages; for
the remaining overrides that legitimately need CSS (the :deep(...) selectors
targeting [data-testid='node-widgets'] and the field-sizing: content for
multiline textareas), stop forcing layout via external !important overrides and
instead add a prop/variant or class to the NodeWidgets component (or its
consumer) so NodeWidgets can apply a one-column grid when used inside
PanelBlockList (reference selectors: panel-block__input
:deep([data-testid='node-widgets']), panel-block__input
:deep([data-testid='node-widget']), and
panel-block__input[data-multiline='true'] :deep(textarea)), and keep only those
minimal scoped rules without !important if unavoidable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/appMode/layout/cells/FeedbackCell.vue`:
- Line 26: Remove the unused align="start" attribute from the
TypeformPopoverButton usage: the component (TypeformPopoverButton) doesn't
declare an align prop and forwards unknown attributes via $attrs to its inner
Button, which also doesn't accept align, causing a stray HTML attribute; edit
the template where TypeformPopoverButton is instantiated (the line with
WIDGET_ID) and delete the align="start" attribute so it no longer propagates to
Button.

In `@src/components/appMode/layout/design-tokens.css`:
- Around line 35-43: Summary: The CSS fallbacks hard-code a dark theme; replace
them with neutral/system fallbacks so the component won’t force dark colors if
the --p-* Primer variables are missing. Update the var(...) fallback values for
the tokens mentioned (e.g., --layout-color-canvas, --layout-color-cell-fill,
--layout-color-cell-hover, --layout-color-text, --layout-color-text-muted,
--layout-color-accent-foreground) to use system color keywords or neutral
defaults (for example use Canvas / CanvasText or a neutral light/dark pair)
instead of hard-coded dark hexes so the layout follows the host/user theme when
--p-* vars are absent. Ensure accent fallbacks are also non-theme-forcing (use
transparent or inherit) for --layout-color-accent-foreground.

In `@src/components/appMode/layout/LayoutGrid.vue`:
- Around line 95-101: The resolvePos function currently maps negative indices
with total + 2 + v which makes resolvePos(-1, N) = N+1 (off the end); either
update the logic or the JSDoc so they match. Fix by changing resolvePos to use
total + 1 + v (so resolvePos(-1, N) = N and resolvePos(-2, N) = N-1) if you want
-1 to mean last track, and update any callers/tests accordingly; alternatively,
if the current math is intended, update the JSDoc in LayoutView.vue (the cells
comment) to remove the “-1 = last track” wording so it reflects the actual
mapping. Reference: resolvePos and the cells JSDoc in LayoutView.vue.
- Around line 29-54: The component currently uses withDefaults(defineProps(...))
which is disallowed in Vue 3.5+; replace the runtime with a compile-time
reactive destructuring pattern: call defineProps<typeof Props>() (or defineProps
with the same interface) and then immediately destructure into reactive/local
constants for cellSize, outerPadding, minGap, fillEmpty, and cells instead of
using props.*, updating all usages of props.trackCount / props.axisGap /
props.ghostCells / props.gridStyle (or any other props.* references) to the
destructured names (trackCount, axisGap, ghostCells, gridStyle) so the component
relies on the new destructured props rather than withDefaults.

In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 382-396: The watch on () => selectedHistory.value?.output.url
should add an img.onerror handler so failed image loads don't leave dimensions
stuck; inside the same callback where you create const img = new Image() and set
img.onload, also set img.onerror = () => { /* clear/confirm state */
dimensions.value = null; optionally log the error or URL for debugging }, and
ensure this runs before assigning img.src = url to guard against
404/CORS/network failures for the selectedHistory/output URL.
- Around line 334-361: Make loadSelectedWorkflow return a success boolean (or
throw) when it actually loads the intended workflow so callers can abort if
selectedHistory is null or extraction fails; inside loadSelectedWorkflow use the
existing workflowStore reference instead of calling useWorkflowStore() again
when accessing changeTracker. Update actionRerun to await loadSelectedWorkflow
and return early if it indicates failure, and replace the console.error in
actionRerun's catch with the already-destructured toastErrorHandler to show
users an error toast when commandStore.execute('Comfy.QueuePrompt', ...) fails.
- Around line 315-330: The selectedHistory computed currently uses id.split(':')
which fails if asset IDs contain colons; change parsing to locate the final
colon with lastIndexOf and extract assetId and outputIndex from substrings:
verify id startsWith 'history:', find lastColon = id.lastIndexOf(':'), let
assetId = id.slice('history:'.length, lastColon) and kStr = id.slice(lastColon +
1), then Number(kStr) for outputIndex and proceed to find asset via
outputs.media.value and output via allOutputs(asset) as before, with the same
null/NaN checks.

In `@src/components/appMode/layout/panels/FloatingPanel.vue`:
- Around line 11-19: Replace the runtime withDefaults props declaration in
FloatingPanel.vue with Vue 3.5 TypeScript-style reactive destructuring: use
defineProps<{ preset: PanelPreset; title?: string; dragging?: boolean;
onHeaderPointerDown?: (e: PointerEvent) => void }>() to get typed props, then
immediately destructure into reactive defaults (e.g. const { preset, title =
undefined, dragging = false, onHeaderPointerDown = undefined } =
defineProps<...>()) so template and script use plain references (preset, title,
dragging, onHeaderPointerDown) instead of props.preset; keep types and default
values identical to the existing withDefaults usage.

In `@src/components/appMode/layout/panels/PanelBlockList.vue`:
- Around line 22-33: Replace the onReorder callback prop with a Vue emit: remove
onReorder from the defineProps signature and add a defineEmits<{ (e: 'reorder',
from: BlockPos, target: DropTarget): void }>() declaration, then update the
useBlockDrag onCommit handler to call emit('reorder', from, target) instead of
props.onReorder?.(from, target); update any callers (e.g., LayoutView) to listen
for `@reorder` instead of passing an onReorder prop and adjust references to
props/onReorder accordingly (symbols: defineProps, onReorder, defineEmits,
useBlockDrag, onCommit).
- Around line 43-60: showRowIndicator and showColIndicator currently allocate a
new computed on each call and shadow the i18n variable `t`; change both
functions to return a plain boolean (evaluate against dropTarget.value directly)
instead of computed refs and rename the local dropTarget value variable (e.g.,
dt) to avoid shadowing the `t` from useI18n(); then update template call sites
to use showRowIndicator(rowIdx) and showColIndicator(rowIdx, colIdx) (remove the
trailing .value).
- Around line 93-95: The aria-label interpolation currently inserts raw
block.kind (e.g., 'input', 'run'); update PanelBlockList.vue so block.kind is
translated before being interpolated into t('linearMode.blockDragReorderAria').
Either call the translator with a composed key (e.g.,
t(`linearMode.blockKind.${block.kind}`)) or map block.kind to
t('input')/t('run') in a small helper/computed property (e.g.,
computedTranslatedKind) and pass that translated string into
t('linearMode.blockDragReorderAria', { kind: computedTranslatedKind }), ensuring
the aria-label uses the translated human-readable block type.

In `@src/components/appMode/layout/panels/useBlockDrag.ts`:
- Around line 82-142: The window-level pointer handlers (onPointerMove,
onPointerUp, onPointerCancel) must ignore events from other pointers; add an
early return when e.pointerId !== activePointerId. Change onPointerUp and
onPointerCancel to accept the PointerEvent param, and in onPointerMove,
onPointerUp, and onPointerCancel check activePointerId !== null and return if
e.pointerId !== activePointerId before proceeding (so computeDropTarget and
endDrag only run for the captured pointer). Use the existing activePointerId,
startDrag, endDrag, computeDropTarget, and dropTarget symbols to locate where to
add these checks.

In `@src/renderer/extensions/linearMode/LinearWelcome.vue`:
- Around line 26-34: The runFromPill function currently swallows failures from
commandStore.execute by only logging the error; update runFromPill to surface
failures to higher layers by either rethrowing the caught error after logging or
invoking a UI error handler so users see the failure; specifically modify the
catch block in runFromPill (which calls commandStore.execute with commandId and
metadata) to call the app's error/display function (or throw error) instead of
just console.error, ensuring commandId and the original error are preserved when
passed upward.
- Around line 65-67: The Run button's current class removes all focus outlines
("focus:outline-none") which makes keyboard focus invisible; restore an
accessible visible focus state by replacing or supplementing that rule with a
focus-visible style on the same button element in LinearWelcome.vue (the primary
Run button) — e.g. keep "focus:outline-none" if needed for mouse but add a
"focus-visible:..." class that applies a visible outline/ring (such as
focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-{color}) so
keyboard users see focus; update the button's class attribute (the element with
class string containing "focus:outline-none") to include the focus-visible
variant and ensure the visual is distinct for the primary action.

In `@src/views/LinearView.vue`:
- Around line 121-151: The new showLayoutView branch removed the drag-drop entry
so workflow files cannot be imported; add drop handling and ensure the handler
has a valid target by either (A) wiring `@drop`="dragDrop" and `@dragover.prevent`
on the center container (e.g., the SplitterPanel or the parent div used when
showLayoutView is true) and mounting LinearControls so linearWorkflowRef exists,
then call linearWorkflowRef.value?.handleDragDrop(e) inside dragDrop, or (B)
implement equivalent file-import logic directly inside LayoutView (expose a
handleDragDrop method on the LayoutView component and call it from the bound
`@drop`) so the drop works even when LinearControls is not mounted; reference
LinearControls, linearWorkflowRef, dragDrop, and LayoutView to locate where to
add/route the handler.

---

Nitpick comments:
In `@src/components/appMode/layout/cells/BatchCountCell.vue`:
- Around line 18-21: Remove the unused script-level i18n import and binding:
delete the call to useI18n() and the const { t } = useI18n() declaration in
BatchCountCell.vue, and update the template to call the global
$t('linearMode.runCount') instead of relying on the removed t; ensure no other
script references to t remain and keep storeToRefs(useQueueSettingsStore()) and
useSettingStore() unchanged.

In `@src/components/appMode/layout/cells/FeedbackCell.vue`:
- Around line 57-75: The CSS in FeedbackCell.vue currently uses deep selectors
with !important to override TypeformPopoverButton's variant="inverted"; instead,
add a muted/ghost variant (or expose a variant prop pass-through) to
TypeformPopoverButton and use that variant in FeedbackCell, or remove
variant="inverted" and rely on $attrs/class forwarding so the caller can pass
transparent/muted classes; then delete the .feedback-cell :deep(button|a) rules
and the !important overrides (and the related hover and icon sizing deep
selectors if unnecessary), ensuring FeedbackCell uses the new variant or
forwarded classes instead of style overrides.

In `@src/components/appMode/layout/cells/IconCell.vue`:
- Around line 17-19: Replace the onActivate prop-callback pattern in
IconCell.vue with Vue emits: remove the onActivate prop from the props/type
declaration, add defineEmits<{ activate: [] }>() and call emit('activate') where
onActivate() was previously invoked; update any TypeScript types if needed and
ensure consumers (LayoutView.vue) keep using `@activate`="..." (no changes
required there) so the component uses the idiomatic emit('activate') event
instead of a callback prop.

In `@src/components/appMode/layout/cells/InputCell.vue`:
- Around line 96-106: The current CSS in InputCell.vue uses :deep selectors with
!important to override widget styles, which masks the real fix; instead update
the widget components (WidgetTextarea, WidgetInputNumber) to respect the layout
context by checking the injected HideLayoutFieldKey or a provided layout flag
and apply a conditional class that disables their text-xs/explicit
height/resizable styles when rendered inside InputCell; remove the !important
overrides from InputCell.vue (or replace them with a simple non-!important
selector) and add a TODO comment referencing HideLayoutFieldKey and the
conditional class approach so downstream code uses the widget-level fix rather
than forcing overrides here.

In `@src/components/appMode/layout/cells/ModeToggleCell.vue`:
- Around line 22-47: The scoped CSS in ModeToggleCell.vue is using deep
selectors and !important to override WorkflowActionsDropdown internals; instead,
add a layout-specific prop/class hook to WorkflowActionsDropdown (e.g., a
variant or className prop) that applies the transparent background, zero padding
and full-width Tailwind utilities from the dropdown root, then use that prop
when rendering <WorkflowActionsDropdown> inside ModeToggleCell and remove the
deep :deep(...) rules and !important overrides; also replace remaining styling
with Tailwind utility classes on the ModeToggleCell root and the new exposed
dropdown variant so no scoped style block or !important usage is required.

In `@src/components/appMode/layout/cells/OutputThumbCell.vue`:
- Around line 50-64: Compute getMediaType(output) once by creating a computed
property (e.g., mediaType = computed(() => getMediaType(output))) in the
OutputThumbCell component and use that computed value in the template
v-if/v-else-if instead of calling getMediaType(output) twice; update the script
block to import/use Vue's computed (or add a computed option if using Options
API) so mediaType updates when output changes and replace both template calls
with mediaType === 'images' and mediaType === 'video'.

In `@src/components/appMode/layout/cells/RunCell.vue`:
- Around line 17-19: The handleClick function is over-narrowing the event;
change its signature to accept a MouseEvent (async function handleClick(e:
MouseEvent)) and remove the `'shiftKey' in e` check and the cast—compute
priority directly with e.shiftKey and keep the commandId logic (const priority =
e.shiftKey; const commandId = priority ? 'Comfy.QueuePromptFront' :
'Comfy.QueuePrompt'). This simplifies typing and removes the unnecessary
KeyboardEvent | MouseEvent cast while keeping the behavior intact.
- Around line 34-42: Replace the raw <button> in RunCell.vue with the shared
Button component from src/components/ui/button/Button.vue: import and register
Button in the component, use <Button> in place of the element and forward
props/data attributes (type="button", data-testid="layout-run-cell") and the
click handler (handleClick) as `@click`. Map the cell styling to an existing
Button prop (e.g., variant="accent") and if no suitable variant exists, add a
new variant to Button.vue (styling/token) so the run-cell appearance comes from
the shared Button rather than a bespoke element; keep the icon and label in the
Button slot. Ensure component name references: RunCell.vue, handleClick, and
Button.vue.

In `@src/components/appMode/layout/panels/FloatingPanel.vue`:
- Around line 29-32: Replace the callback prop pattern: remove the
onHeaderPointerDown prop from FloatingPanel and instead emit a
'header-pointer-down' event from the header handler (use
$emit('header-pointer-down', $event) or defineEmits in script setup); update
FloatingPanel's template to use `@pointerdown` to emit rather than calling the
prop. Update any TypeScript/prop typings accordingly (remove prop type and
adjust emits type if present). Finally, update the parent (LayoutView) to listen
with `@header-pointer-down`="onHeaderPointerDown" (keeping its handler name) so
the parent behavior is preserved.

In `@src/components/appMode/layout/panels/PanelBlockList.vue`:
- Around line 143-285: The scoped <style> uses many rules that should be
converted to Tailwind utilities and it misuses !important; remove or migrate the
bulk of layout rules (panel-block-list, panel-block-row, panel-block,
panel-block__grip, panel-block__content, panel-block__run-batch, etc.) into
Tailwind 4 classes on the corresponding template elements, and eliminate all
`!important` usages; for the remaining overrides that legitimately need CSS (the
:deep(...) selectors targeting [data-testid='node-widgets'] and the
field-sizing: content for multiline textareas), stop forcing layout via external
!important overrides and instead add a prop/variant or class to the NodeWidgets
component (or its consumer) so NodeWidgets can apply a one-column grid when used
inside PanelBlockList (reference selectors: panel-block__input
:deep([data-testid='node-widgets']), panel-block__input
:deep([data-testid='node-widget']), and
panel-block__input[data-multiline='true'] :deep(textarea)), and keep only those
minimal scoped rules without !important if unavoidable.

In `@src/components/appMode/layout/panels/PanelDragPreview.vue`:
- Around line 45-88: Duplicate preset positioning CSS in PanelDragPreview.vue
(classes .panel-drag-preview--right-dock, .panel-drag-preview--left-dock,
.panel-drag-preview--float-tr/br/tl/bl) should be extracted so it stays in sync
with FloatingPanel.vue; create a single, unscoped shared stylesheet (e.g.,
panelPresets.css) that defines .panel-preset--right-dock,
.panel-preset--left-dock and .panel-preset--float-tr/br/tl/bl using the same
vars, then update FloatingPanel.vue and PanelDragPreview.vue to compose or reuse
those shared classes/selectors (via `@apply`, multiple class names, or a shared
selector) instead of duplicating the six rules. Ensure the new shared file is
imported globally or into both components so the presets remain the single
source of truth.

In `@src/components/appMode/layout/panels/panelTypes.ts`:
- Around line 29-45: Remove the unnecessary "...Base" indirection by collapsing
InputBlockBase and RunBlockBase into their exported types: replace the separate
interface declarations InputBlockBase and RunBlockBase with direct exported
types/interfaces named InputBlock and RunBlock (keeping all existing fields: id,
kind, entryKey, isMultiline for InputBlock and id, kind, withBatchCount for
RunBlock), and update any other code that imports or references
InputBlockBase/RunBlockBase to use InputBlock/RunBlock instead of the Base
names; ensure JSDoc/comments (e.g., the entryKey comment) are preserved on the
final types.

In `@src/components/appMode/layout/panels/usePanelDrag.ts`:
- Around line 82-90: The cleanup in onPointerUp should reset the pointer-capture
state before calling the commit callback to avoid leaving stale state if
opts.onCommit throws; move the assignments activePointerId = null and capturedEl
= null (and keep removing the window listeners) so they occur before the
conditional opts.onCommit(snapTarget.value), i.e. clear
capturedEl/activePointerId first, then call opts.onCommit when snapTarget
differs from opts.currentPreset.
- Line 13: PANEL_HALF_WIDTH is a hardcoded constant that can drift from
FloatingPanel's CSS variable --panel-dock-width, causing preset anchor math in
presetAnchors to misalign; replace the constant with a small helper (e.g.,
panelHalfWidth()) that reads
getComputedStyle(document.documentElement).getPropertyValue('--panel-dock-width'),
parses to px with a 420px fallback and returns half the value, and call that
helper inside presetAnchors (and any other places using PANEL_HALF_WIDTH) so
anchors recompute from the actual CSS value rather than a fixed 210.
- Around line 115-119: The onBeforeUnmount cleanup currently only removes window
listeners and leaves an in-flight drag state (capturedEl pointer capture never
released and onCommit not invoked); modify the onBeforeUnmount handler in
usePanelDrag to call endDrag() (or explicitly call the same teardown/commit
logic used by endDrag) before removing listeners so pointer capture on
capturedEl is released, onCommit runs (or the commit is skipped intentionally),
and internal drag state is reset.

In `@src/components/appMode/layout/README.md`:
- Around line 1-35: The README links to a private repo (`comfyui-app-mode`) and
its `todo.md`, which will confuse external contributors; update the README.md to
either remove the direct link to `comfyui-app-mode` and `todo.md`, or replace it
with a clear note that the referenced repo is private and provide a public
summary of the phase map (or extract the relevant `Phase map` contents into this
README). Edit the README.md header where `comfyui-app-mode` is mentioned and the
Phase map paragraph to either omit the private link or add a short public-facing
summary and a “private repo — contact owner” note so public docs don’t point to
404s.

In `@src/renderer/extensions/linearMode/LinearPreview.vue`:
- Around line 158-161: The comment above the OutputHistory element contains a
duplicated word "layout / layout"; edit the comment in LinearPreview.vue so it
reads "even when the layout hides this bar" (keep the rest of the comment
intact), ensuring references to OutputHistory, v-show, updateSelection,
selectedItem/selectedOutput remain unchanged.

In `@src/renderer/extensions/linearMode/LinearWelcome.test.ts`:
- Around line 40-44: The test currently mocks useCommandStore's execute but
doesn't assert the new behaviors; update LinearWelcome.test.ts to assert that
clicking the "Run" button calls the command store's execute with
"Comfy.QueuePrompt" and that shift-clicking the same button calls it with
"Comfy.QueuePromptFront". Locate the mocked useCommandStore
(vi.mock('@/stores/commandStore') returning execute: vi.fn()), simulate a normal
click and a click with shiftKey (e.g., fireEvent or userEvent with { shiftKey:
true }), then add expectations asserting execute was called with those exact
command names (and appropriate payload if applicable); also ensure you clear or
reset the mock between interactions so calls are isolated.

In `@src/views/LinearView.vue`:
- Around line 126-132: The Splitter used inside the showLayoutView branch
(component Splitter with state-key="Comfy.LinearView.SidebarPanelSplitter",
state-storage="local", `@resizestart`="$event.originalEvent.preventDefault()", and
fixed :size props) is effectively non-resizable and persists widths that never
change; replace it with a simple flex row layout (use container classes like
flex/row and two children with the same fixed sizes or flex-basis) and render a
non-interactive visual divider/gutter instead of the PrimeVue Splitter so the UI
no longer shows a draggable gutter that does nothing while preserving the
current panel widths and appearance and removing state-storage and resizestart
usage.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8a767213-6272-495e-85c7-647bf2f3c5fe

📥 Commits

Reviewing files that changed from the base of the PR and between 2d50cc2 and a4addaa.

📒 Files selected for processing (29)
  • src/components/appMode/layout/LayoutGrid.vue
  • src/components/appMode/layout/LayoutView.vue
  • src/components/appMode/layout/README.md
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/components/appMode/layout/cells/FeedbackCell.vue
  • src/components/appMode/layout/cells/IconCell.vue
  • src/components/appMode/layout/cells/InputCell.vue
  • src/components/appMode/layout/cells/ModeToggleCell.vue
  • src/components/appMode/layout/cells/OutputThumbCell.vue
  • src/components/appMode/layout/cells/RunCell.vue
  • src/components/appMode/layout/design-tokens.css
  • src/components/appMode/layout/panels/FloatingPanel.vue
  • src/components/appMode/layout/panels/PanelBlockList.vue
  • src/components/appMode/layout/panels/PanelDragPreview.vue
  • src/components/appMode/layout/panels/panelTypes.ts
  • src/components/appMode/layout/panels/useBlockDrag.ts
  • src/components/appMode/layout/panels/usePanelDrag.ts
  • src/locales/en/main.json
  • src/platform/workflow/management/stores/comfyWorkflow.ts
  • src/platform/workflow/validation/schemas/workflowSchema.test.ts
  • src/platform/workflow/validation/schemas/workflowSchema.ts
  • src/renderer/extensions/linearMode/AppModeWordmark.vue
  • src/renderer/extensions/linearMode/ImagePreview.vue
  • src/renderer/extensions/linearMode/LinearPreview.vue
  • src/renderer/extensions/linearMode/LinearWelcome.test.ts
  • src/renderer/extensions/linearMode/LinearWelcome.vue
  • src/renderer/extensions/linearMode/MediaOutputPreview.vue
  • src/renderer/extensions/linearMode/VideoPreview.vue
  • src/views/LinearView.vue

Comment thread src/components/appMode/layout/cells/FeedbackCell.vue Outdated
Comment on lines +35 to +43
--layout-color-canvas: var(--p-content-background, #1a1a1a);
--layout-color-cell-fill: var(--p-surface-800, #2a2a2a);
--layout-color-cell-hover: var(--p-surface-700, #3a3a3a);
--layout-color-text: var(--p-text-color, #fafafa);
--layout-color-text-muted: var(--p-text-muted-color, #a1a1a1);
--layout-color-accent: #2da44e; /* GitHub Primer green */
--layout-color-accent-hover: #2c974b;
--layout-color-accent-active: #298e46;
--layout-color-accent-foreground: #ffffff;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Nit: hex fallbacks hard-code a dark theme.

If the --p-* PrimeVue variables are ever missing (e.g., a theme-less render path), the var(..., #1a1a1a) / #fafafa fallbacks produce a dark surface with light text regardless of user theme. Not blocking — just worth being aware of if App Mode ever needs to render standalone or in a light theme.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/design-tokens.css` around lines 35 - 43,
Summary: The CSS fallbacks hard-code a dark theme; replace them with
neutral/system fallbacks so the component won’t force dark colors if the --p-*
Primer variables are missing. Update the var(...) fallback values for the tokens
mentioned (e.g., --layout-color-canvas, --layout-color-cell-fill,
--layout-color-cell-hover, --layout-color-text, --layout-color-text-muted,
--layout-color-accent-foreground) to use system color keywords or neutral
defaults (for example use Canvas / CanvasText or a neutral light/dark pair)
instead of hard-coded dark hexes so the layout follows the host/user theme when
--p-* vars are absent. Ensure accent fallbacks are also non-theme-forcing (use
transparent or inherit) for --layout-color-accent-foreground.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 299292a69. design-tokens.css is deleted — it was just a temporary placeholder. Tokens now live in packages/design-system/src/css/layout.css as @theme entries with no hex fallbacks.

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.

@eliheuer Thanks for the update! That's a clean resolution — deleting design-tokens.css and consolidating everything into packages/design-system/src/css/layout.css as @theme entries (backed by PrimeVue runtime variables with no hex fallbacks) fully addresses the concern. ✅

( \ /)
( •ω•)ノ
( づ🎉


🧠 Learnings used
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-20T01:57:38.157Z
Learning: Applies to **/*.vue : Never use the `dark:` Tailwind variant, instead use semantic values from the `style.css` theme (e.g., `bg-node-component-surface`)

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-20T01:57:33.633Z
Learning: Applies to **/*.vue : Use Tailwind semantic values from style.css theme instead of dark: variant (e.g., bg-node-component-surface instead of dark:bg-*)

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-20T01:57:38.157Z
Learning: Before implementing user-facing features, consult the Comfy Design Standards Figma file

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment thread src/components/appMode/layout/LayoutGrid.vue Outdated
Comment thread src/components/appMode/layout/LayoutGrid.vue Outdated
Comment thread src/components/appMode/layout/LayoutView.vue Outdated
Comment thread src/components/appMode/layout/panels/PanelBlockList.vue Outdated
Comment thread src/components/appMode/layout/panels/useBlockDrag.ts Outdated
Comment thread src/renderer/extensions/linearMode/LinearWelcome.vue
Comment thread src/renderer/extensions/linearMode/LinearWelcome.vue Outdated
Comment thread src/views/LinearView.vue
Comment on lines +121 to +151
<div
v-if="showLayoutView"
class="flex h-[calc(100%-var(--workflow-tabs-height))] w-full"
>
<SideToolbar />
<Splitter
v-if="activeTab"
class="flex-1 border-none bg-transparent"
state-key="Comfy.LinearView.SidebarPanelSplitter"
state-storage="local"
@resizestart="$event.originalEvent.preventDefault()"
>
<SplitterPanel
:size="SIDE_PANEL_SIZE"
:min-size="SIDEBAR_MIN_SIZE"
class="min-w-78 overflow-hidden"
>
<div
class="size-full overflow-x-hidden border-r border-border-subtle"
>
<ExtensionSlot :extension="activeTab" />
</div>
</SplitterPanel>
<SplitterPanel :size="100 - SIDE_PANEL_SIZE" class="relative">
<LayoutView />
</SplitterPanel>
</Splitter>
<div v-else class="relative flex-1">
<LayoutView />
</div>
</div>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find any drop / drag-drop handling inside the new LayoutView + App Mode tree
fd -e vue . src/components/appMode | xargs rg -n -C2 -P '@drop|ondrop|handleDragDrop|\bdrop\s*\(|DropZone'
rg -n -C2 -P 'handleDragDrop' src/renderer/extensions/linearMode

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2902


🏁 Script executed:

# Find LayoutView component definition
fd -type f -name "LayoutView.vue" src/

# Check what DropZone does - is it for workflows or inputs?
cat -n src/renderer/extensions/linearMode/DropZone.vue | head -100

# Check if LayoutView has any drop/drag handlers
rg -n "@drop|ondrop|@dragover|dragDrop" src/views/LinearView.vue

# Check if LinearControls is still mounted anywhere in the showLayoutView context
rg -n "LinearControls" src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4035


🏁 Script executed:

# Get a clearer view of LinearView.vue structure around the new showLayoutView branch
cat -n src/views/LinearView.vue | sed -n '100,250p'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 6075


🏁 Script executed:

# Find LayoutView component definition to check if it handles drag-drop
find src -name "LayoutView.vue" -type f

# Check LayoutView implementation for drag-drop handling
cat -n src/views/LayoutView.vue | head -150

# Verify: does LayoutView have `@drop` or dragDrop handling?
rg -n "@drop|dragDrop|useDropZone" src/views/LayoutView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 253


🏁 Script executed:

# Check LayoutView.vue at the correct path
cat -n src/components/appMode/layout/LayoutView.vue | head -150

# Verify: does LayoutView have `@drop` or dragDrop handling?
rg -n "@drop|dragDrop|useDropZone" src/components/appMode/layout/LayoutView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 6699


🏁 Script executed:

# Check for drop/drag handling further down in LayoutView.vue
rg -n "@drop|dragDrop|handleDragDrop|useDropZone" src/components/appMode/layout/LayoutView.vue

# Also check full length to see if file ends shortly
wc -l src/components/appMode/layout/LayoutView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 118


🏁 Script executed:

# Check LayoutView template structure for drop handlers
tail -n 100 src/components/appMode/layout/LayoutView.vue | head -n 80

# More specifically, search the entire file for drop/drag patterns
rg -n -i "drop|drag" src/components/appMode/layout/LayoutView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3588


🏁 Script executed:

# Check the outer container div in LinearView - does it have `@drop`?
sed -n '110,125p' src/views/LinearView.vue

# Also verify: is there anything in LayoutView's template root that might catch drops?
sed -n '600,640p' src/components/appMode/layout/LayoutView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2160


🏁 Script executed:

# Final verification: confirm outer container has no `@drop` binding in showLayoutView path
sed -n '121,151p' src/views/LinearView.vue

# And confirm the old branch HAS `@drop` on the center panel
sed -n '189,210p' src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1940


Add drag-drop workflow import handling to the new App Mode layout.

The new showLayoutView branch does not support drag-dropping workflow files (.json/.png) onto the app. The previous branch had @drop="dragDrop" bound to the center panel, which called linearWorkflowRef.value?.handleDragDrop(e). In the new branch:

  • Neither the outer container nor LayoutView binds @drop
  • LinearControls is not mounted, so linearWorkflowRef never exists — even if a @drop handler were added, it would silently fail
  • LayoutView contains no workflow file drop handling (its drag references are for panel repositioning only)

Either wire @drop="dragDrop" to a container in the new branch and ensure LinearControls (or equivalent) is mounted to populate linearWorkflowRef, or implement workflow import handling directly in LayoutView.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/LinearView.vue` around lines 121 - 151, The new showLayoutView
branch removed the drag-drop entry so workflow files cannot be imported; add
drop handling and ensure the handler has a valid target by either (A) wiring
`@drop`="dragDrop" and `@dragover.prevent` on the center container (e.g., the
SplitterPanel or the parent div used when showLayoutView is true) and mounting
LinearControls so linearWorkflowRef exists, then call
linearWorkflowRef.value?.handleDragDrop(e) inside dragDrop, or (B) implement
equivalent file-import logic directly inside LayoutView (expose a handleDragDrop
method on the LayoutView component and call it from the bound `@drop`) so the drop
works even when LinearControls is not mounted; reference LinearControls,
linearWorkflowRef, dragDrop, and LayoutView to locate where to add/route the
handler.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think this is a false positive. The old @drop="dragDrop" binding didn't route workflow file imports — those are handled at the document level by app.addDropHandler() in src/scripts/app.ts:589, which works regardless of which LinearView branch renders.

What dragDrop actually did was route per-widget drops through LinearControls.handleDragDropAppModeWidgetList.handleDragDrop (iterating selected node widgets). In the new branch those still work because each InputCell.vue wraps its widgets in a <DropZone>. The routing moved from one central relay to per-cell zones.

No code change needed.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Verify document-level drop handler in app.ts around line 589
sed -n '580,600p' src/scripts/app.ts

# 2. Check what dragDrop/handleDragDrop in LinearControls actually does
rg -n "handleDragDrop" src/renderer/extensions/linearMode/LinearControls.vue -A 10 | head -40

# 3. Confirm DropZone usage in InputCell.vue
rg -n "DropZone" src/components/appMode/layout/cells/InputCell.vue

Length of output: 1462


@eliheuer, you're right — I've verified all three points:

  1. Workflow file imports: app.addDropHandler() in src/scripts/app.ts registers a document-level drop listener that handles .json/.png workflow imports regardless of which LinearView branch is active. ✅
  2. dragDrop scope: LinearControls.handleDragDrop (Line 74) just delegates to widgetListRef.value?.handleDragDrop(e) — it was only ever a relay for per-widget drops, not file imports. ✅
  3. Per-widget drops in the new branch: InputCell.vue (Lines 65–70) wraps NodeWidgets in <DropZone>, so per-cell drops work without the central relay. ✅

The comment was a false positive. Sorry for the noise!

( ·̥̥̥̥̥̥̥̥ω·̥̥̥̥̥̥̥̥ ) noted for future reviews


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/views/LinearView.vue:121-151
Timestamp: 2026-04-22T17:12:51.213Z
Learning: In `src/views/LinearView.vue`, the `dragDrop` / `drop` binding on the center splitter panel in the old `LinearView` branch was NOT responsible for workflow file imports (.json/.png). Workflow file imports are handled at the document level by `app.addDropHandler()` in `src/scripts/app.ts` via `useEventListener(document, 'drop', ...)`, which fires regardless of which branch renders. The `dragDrop` relay only routed per-widget drops to `LinearControls.handleDragDrop` → `AppModeWidgetList.handleDragDrop`. In the new `showLayoutView` branch, per-widget drops work through per-cell `<DropZone>` wrappers in `InputCell.vue`. Do not flag the absence of `drop="dragDrop"` in the new App Mode layout branch as a regression.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/PanelBlockList.vue:0-0
Timestamp: 2026-04-22T17:03:35.095Z
Learning: In `src/components/appMode/layout/panels/PanelBlockList.vue`, the block-drag grip element was intentionally demoted from a `<button>` to a `<span aria-hidden="true">` (commit 385b611a0) to remove it from the accessibility tree pending a proper keyboard-reorder implementation. The `linearMode.blockDragReorderAria` i18n key is kept in `src/locales/en/main.json` as a forward-compatible placeholder for when keyboard reorder lands as a follow-up. Do not flag the aria-hidden grip or the unused translation key as issues.

Learnt from: jtydhr88
Repo: Comfy-Org/ComfyUI_frontend PR: 11541
File: src/renderer/extensions/vueNodes/components/LGraphNode.vue:816-825
Timestamp: 2026-04-22T03:35:47.385Z
Learning: In `src/renderer/extensions/vueNodes/components/LGraphNode.vue`, the `handleDrop` function intentionally does NOT wrap `await node.onDragDrop(event)` in a try/catch or route errors through `toastErrorHandler`. Previously no error popup existed for this path, and adding one would be new behavior out of scope. Do not flag the missing try/catch around `node.onDragDrop` in `handleDrop` as an issue; if error surfacing is needed it should be a separate PR.

Learnt from: jtydhr88
Repo: Comfy-Org/ComfyUI_frontend PR: 11495
File: src/extensions/core/load3d/load3dContextMenuGuard.ts:52-66
Timestamp: 2026-04-21T03:30:28.591Z
Learning: In `src/extensions/core/load3d/load3dContextMenuGuard.ts`, the contextmenu handler intentionally returns early (before `preventDefault`/`stopPropagation`) when a drag is detected. This is pre-refactor behavior that was deliberately preserved. Do not flag the early return on drag as a bug — the native context menu appearing on drag is accepted behavior in this codebase.

Learnt from: jtydhr88
Repo: Comfy-Org/ComfyUI_frontend PR: 11538
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue:47-51
Timestamp: 2026-04-22T02:47:43.665Z
Learning: In Comfy-Org/ComfyUI_frontend, `findServerPreviewUrl` (src/platform/assets/utils/assetPreviewUtil.ts) is typed `Promise<string | null>` and contains an internal try/catch that returns `null` on any error — it never rejects. Call sites (e.g., FormDropdownMenuItem.vue, Media3DTop.vue) do not need an outer try/catch; error handling lives entirely inside the utility.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-20T01:57:38.157Z
Learning: Before implementing user-facing features, consult the Comfy Design Standards Figma file

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Replaces the builder's splitter-sidebar with the same FloatingPanel
App Mode uses, so what authors see while picking + arranging inputs
is exactly what end-users get at runtime. Preset + collapsed state
live on appModeStore, so moving or collapsing in either view updates
the other.

FloatingPanel gains a Vizcom-style collapsible header (chevron + title
+ 3-dot menu), distinct header/footer tint via `--layout-color-header-fill`,
and owns drag-to-snap + collapse internally. AppModeWidgetList + InputCell
share typography and subtitle treatment so both rendering paths read
identically; `friendlyNodeLabel` strips parens-annotated node names
("CLIP Text Encode (Positive Prompt)" → "Positive Prompt") and
collapses noisy titles via a short-label map ("Empty Latent Image"
→ "Image").

Polish pass for the demo: 10px vertical rhythm across label → input
→ next-label, 8px Run button radius to match widget textareas,
widget inputs on canvas-dark, LinearWelcome typography + left
alignment so the welcome copy lines up with the left toolbar chrome.

Builder toolbars + BuilderMenu adopt the same panel-chrome surface
and design tokens, promoted from `.layout-view` scope to `:root`
so everything shares one visual language.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/builder/AppBuilder.vue (1)

183-195: ⚠️ Potential issue | 🔴 Critical

Bind the :key attribute to the loop variable in both v-for blocks.

Bare :key is invalid Vue 3 syntax. Use :key="key" instead, binding explicitly to the destructured loop variable.

Affected lines

Line 186: :key:key="key" (in renderedInputs loop)

Line 194: :key:key="key" (in renderedOutputs loop)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/builder/AppBuilder.vue` around lines 183 - 195, The v-for
loops using destructuring (renderedInputs and renderedOutputs) use an invalid
bare :key—update both to bind explicitly to the destructured loop variable by
changing the key attribute to :key="key" in the template guarded by
isSelectInputsMode and the else branch that iterates renderedOutputs; ensure the
same fix is applied where toValue(style) is used for the :style binding so the
elements have a proper unique key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 326-357: loadSelectedWorkflow currently returns before
app.loadGraphData completes, causing actionRerun and actionReuseParams to
operate on the old graph; update loadSelectedWorkflow so every path that calls
app.loadGraphData awaits its completion (i.e., await
app.loadGraphData(workflow)), and then change callers actionRerun and
actionReuseParams to await loadSelectedWorkflow() instead of calling it without
awaiting (replace void/unchecked calls with await) so rerun/reuse always run
after the new graph has fully loaded.
- Around line 547-557: The info chip is rendered as a focusable <button> but has
no action; change the template branch where v-else-if="cell.kind ===
'action-info'" to use a non-interactive element (e.g., <div> or <span>) with the
same class ("info-cell") and bindings (:title="infoTitle", v-if on infoDims, {{
infoLabel }}), remove the button-specific attributes like type="button" and any
focusable behavior so it is not keyboard-focusable, and keep or adjust aria
attributes as needed (e.g., keep :aria-label="infoTitle" only if you want
accessible naming for a non-interactive element).

In `@src/components/appMode/layout/panels/FloatingPanel.vue`:
- Around line 214-218: The .floating-panel--collapsed rule is removing bottom:
which breaks vertical anchoring for presets using bottom-only insets
(float-br/float-bl); update the CSS so collapsing only resets vertical sizing
(height, max-height) and does not unconditionally set bottom: auto — either
remove the bottom: auto declaration from .floating-panel--collapsed or scope it
so it does not apply when the panel has float-br or float-bl (e.g.,
.floating-panel--collapsed:not(.float-br):not(.float-bl) { bottom: auto; }), and
apply the same fix to the other collapsed rules referenced around the same
block.
- Around line 67-85: The panel is committing a snap on simple header clicks
because usePanelDrag currently starts drag on pointerdown and always calls
onCommit on pointerup; update usePanelDrag (and its caller in FloatingPanel.vue)
so a drag only enters "active" state after a small movement threshold (e.g.
minDistance) or set a flag `moved` on pointermove and only invoke the onCommit
callback when `moved` is true; adjust the call-site (handleHeaderPointerDown /
onHeaderPointerDown usage) to pass the new option or to check the moved flag
(instead of relying solely on isDragging) so a plain click does not re-snap the
panel.

In `@src/components/builder/BuilderMenu.vue`:
- Around line 106-113: The trigger is always anchored from the left
(.builder-menu-trigger) so it floats incorrectly when the chrome flips for
right-side sidebars; change the positioning to be direction-aware by replacing
the hard left offset with a logical inline inset (e.g., use inset-inline-start:
calc(var(--sidebar-width, 0px) + var(--layout-outer-padding)) or add a
right-side rule that sets right: calc(...) when the chrome indicates
sidebarLocation is "right") so the trigger sits adjacent to the toolbar in both
left- and right-sidebar layouts.

In `@src/components/builder/BuilderPanel.vue`:
- Around line 90-94: The wrapper element using class
"builder-panel-footer-disabled" currently prevents hover/focus (e.g., via
pointer-events:none) so the title tooltip and not-allowed cursor are
unreachable; change the markup/stylesheet so the wrapper remains interactive
(keep the title="{{t('linearMode.builder.runDisabledHint')}}" and remove
pointer-events:none from the wrapper and/or remove aria-disabled from the
container), and instead apply the disabled state to the inner controls (give the
actionable elements the aria-disabled="true" or disabled attribute and a CSS
class that sets pointer-events:none/cursor:not-allowed on those elements).
Repeat the same change for the second instance referenced around lines 132-140
so tooltips and cursors work while inner controls remain disabled.

In `@src/components/graph/GraphCanvas.vue`:
- Around line 25-27: The SideToolbar is being shown in builder mode but the
overlay component LiteGraphCanvasSplitterOverlay.vue forcibly sets
sidebarPanelVisible = false when isBuilderMode is true, causing buttons to
appear clickable but never open; fix by either restoring the previous guard in
GraphCanvas.vue (change the template conditional to v-if="showUI &&
!isBuilderMode" for the SideToolbar) or update
LiteGraphCanvasSplitterOverlay.vue so it no longer forces sidebarPanelVisible to
false when isBuilderMode is true (allow sidebarPanelVisible to be controlled
normally or add an explicit opt-in flag), and ensure the symbols SideToolbar,
showUI, sidebarPanelVisible and isBuilderMode are the ones modified.

In `@src/components/LiteGraphCanvasSplitterOverlay.vue`:
- Around line 165-168: The offside splitter is still shown when
rightSidePanelVisible is true even in builder mode; update the
showOffsideSplitter computed to also check the builder mode flag so the splitter
is not reserved in builder mode. Specifically, change the computed for
showOffsideSplitter (which currently returns rightSidePanelVisible.value) to
return rightSidePanelVisible.value && !isBuilderMode.value (or the appropriate
injected/prop/ref name for the builder mode flag used by GraphCanvas.vue) so the
splitter is hidden whenever isBuilderMode is true.

In `@src/utils/nodeTitleUtil.ts`:
- Around line 45-51: friendlyNodeLabel should trim the input once and use that
trimmed value everywhere: compute a single trimmed variable (e.g., const t =
title?.trim()), return '' if t is falsy, run the regex against t, lookup
SHORT_NODE_LABELS using t.toLowerCase(), and on fallback return the trimmed t
(not the original title). Update usages of title in friendlyNodeLabel to use
this trimmed variable and keep the existing match/lookup logic unchanged.

---

Outside diff comments:
In `@src/components/builder/AppBuilder.vue`:
- Around line 183-195: The v-for loops using destructuring (renderedInputs and
renderedOutputs) use an invalid bare :key—update both to bind explicitly to the
destructured loop variable by changing the key attribute to :key="key" in the
template guarded by isSelectInputsMode and the else branch that iterates
renderedOutputs; ensure the same fix is applied where toValue(style) is used for
the :style binding so the elements have a proper unique key.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 99406e05-80cb-4069-9db7-02a6d2801769

📥 Commits

Reviewing files that changed from the base of the PR and between a4addaa and e28af8c.

📒 Files selected for processing (22)
  • src/assets/css/style.css
  • src/components/LiteGraphCanvasSplitterOverlay.vue
  • src/components/appMode/layout/LayoutView.vue
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/components/appMode/layout/cells/InputCell.vue
  • src/components/appMode/layout/cells/RunCell.vue
  • src/components/appMode/layout/design-tokens.css
  • src/components/appMode/layout/panels/FloatingPanel.vue
  • src/components/appMode/layout/panels/PanelBlockList.vue
  • src/components/builder/AppBuilder.vue
  • src/components/builder/AppModeWidgetList.vue
  • src/components/builder/BuilderFooterToolbar.vue
  • src/components/builder/BuilderMenu.vue
  • src/components/builder/BuilderPanel.vue
  • src/components/builder/BuilderToolbar.vue
  • src/components/builder/IoItem.vue
  • src/components/graph/GraphCanvas.vue
  • src/locales/en/main.json
  • src/renderer/extensions/linearMode/LinearWelcome.vue
  • src/stores/appModeStore.ts
  • src/utils/nodeTitleUtil.ts
  • src/views/GraphView.vue
💤 Files with no reviewable changes (1)
  • src/components/builder/IoItem.vue
✅ Files skipped from review due to trivial changes (2)
  • src/components/builder/BuilderFooterToolbar.vue
  • src/components/builder/BuilderToolbar.vue
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/renderer/extensions/linearMode/LinearWelcome.vue
  • src/locales/en/main.json

Comment thread src/components/appMode/layout/LayoutView.vue Outdated
Comment thread src/components/appMode/layout/LayoutView.vue Outdated
Comment thread src/components/appMode/layout/panels/FloatingPanel.vue
Comment thread src/components/appMode/layout/panels/FloatingPanel.vue Outdated
Comment thread src/components/builder/BuilderMenu.vue Outdated
Comment thread src/components/builder/BuilderPanel.vue Outdated
Comment thread src/components/graph/GraphCanvas.vue Outdated
Comment thread src/components/LiteGraphCanvasSplitterOverlay.vue Outdated
Comment thread src/utils/nodeTitleUtil.ts Outdated
Move the Run button and "Number of runs" scrubber out of the
FloatingPanel footer into a top-right chrome cluster (with Share
gaining an inline label) so they mirror the graph view and stay
accessible when the panel is collapsed. Offset the right-dock /
float-tr drag previews so they clear the top-row chrome the same
way the left-side presets do.

Anchor the welcome wordmark + body copy to the top of the canvas
and mirror the block to the panel-free side of the viewport:
right-dock panel keeps the copy on the left, left-dock panel
shifts it to the right with right-aligned text. Driven by a
panelSide computed + --welcome-panel-offset-left/right tokens.

Add a JobQueueCell that shows "N active" with the layers icon and
opens the job-history sidebar (falling back to the legacy queue
overlay). Visible only while activeJobsCount > 0; when present
it inserts between BatchCount and Run and shifts the rest of the
cluster 2 tracks left.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (9)
src/renderer/extensions/linearMode/LinearWelcome.vue (2)

88-99: ⚠️ Potential issue | 🟠 Major

Run button still lacks a visible keyboard focus state.

outline-none focus:outline-none without a focus-visible replacement leaves keyboard users with no indication of focus on this primary action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/extensions/linearMode/LinearWelcome.vue` around lines 88 - 99,
The Run button removes keyboard focus styling via the classes outline-none and
focus:outline-none, leaving keyboard users without a visible focus state; update
the button in LinearWelcome.vue (the button that calls runFromPill) to restore
an accessible focus-visible style instead of suppressing it — remove the hard
focus suppression classes and add a visible focus-visible style (e.g., a ring,
outline or box-shadow applied on :focus-visible) so the primary action shows
clear keyboard focus.

35-44: ⚠️ Potential issue | 🟠 Major

Error handling still swallows queue failures and diverges from RunCell.vue.

runFromPill only console.errors failures, so users get no feedback when Queue Prompt fails. The corresponding RunCell.vue implementation (src/components/appMode/layout/cells/RunCell.vue) dispatches the same commands with the same metadata but uses toastErrorHandler from useErrorHandling. Please align with that pattern so these two entrypoints behave consistently.

🛡️ Proposed fix
+import { useErrorHandling } from '@/composables/useErrorHandling'
 import { useCommandStore } from '@/stores/commandStore'
@@
 const commandStore = useCommandStore()
+const { toastErrorHandler } = useErrorHandling()
 async function runFromPill(e: MouseEvent) {
   const commandId = e.shiftKey ? 'Comfy.QueuePromptFront' : 'Comfy.QueuePrompt'
   try {
     await commandStore.execute(commandId, {
       metadata: { subscribe_to_run: false, trigger_source: 'linear' }
     })
   } catch (error) {
-    console.error('[LinearWelcome] Queue prompt failed:', error)
+    toastErrorHandler(error)
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/extensions/linearMode/LinearWelcome.vue` around lines 35 - 44,
In runFromPill, replace the bare console.error swallow with the same
error-handling pattern used in RunCell.vue: import and call
useErrorHandling().toastErrorHandler to surface failures to the user when
commandStore.execute(commandId, { metadata: { subscribe_to_run: false,
trigger_source: 'linear' } }) rejects; keep the try/catch, pass the caught error
and a clear message like "Queue prompt failed" into toastErrorHandler so queue
failures are shown to users instead of only being logged.
src/components/appMode/layout/LayoutView.vue (4)

315-330: ⚠️ Potential issue | 🟡 Minor

Parse history IDs from the final delimiter.

id.split(':') misparses asset IDs that contain :, which can resolve the wrong asset/output pair. Anchor the output index on the last colon after the history: prefix.

🐛 Proposed fix
 const selectedHistory = computed<{
   asset: AssetItem
   output: ResultItemImpl
 } | null>(() => {
   const id = selectedId.value
-  if (!id || !id.startsWith('history:')) return null
-  const [, assetIdStr, kStr] = id.split(':')
-  const assetId = assetIdStr
-  const outputIndex = Number(kStr)
+  const prefix = 'history:'
+  if (!id || !id.startsWith(prefix)) return null
+  const rest = id.slice(prefix.length)
+  const lastColon = rest.lastIndexOf(':')
+  if (lastColon < 0) return null
+  const assetId = rest.slice(0, lastColon)
+  const outputIndex = Number(rest.slice(lastColon + 1))
   if (!assetId || Number.isNaN(outputIndex)) return null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/LayoutView.vue` around lines 315 - 330, The
computed selectedHistory currently uses id.split(':') which breaks when asset
IDs contain colons; update selectedHistory to locate the last colon after the
"history:" prefix (use lastIndexOf(':') or find the index of the last
delimiter), extract assetId as the substring between "history:" and that last
colon and parse outputIndex from the substring after that last colon, then
proceed to look up asset in outputs.media.value and select
allOutputs(asset)[outputIndex] as before to avoid mis-parsing IDs.

599-609: ⚠️ Potential issue | 🟡 Minor

Use non-interactive markup for the info chip.

This renders a focusable button but has no activation behavior. Use a div/span unless this chip performs an action.

♿ Proposed fix
-        <button
+        <div
           v-else-if="cell.kind === 'action-info'"
-          type="button"
           class="info-cell"
           :title="infoTitle"
           :aria-label="infoTitle"
         >
           <i class="info-cell__icon icon-[lucide--file]" />
           <span v-if="infoDims" class="info-cell__dims">{{ infoDims }}</span>
           <span class="info-cell__label">{{ infoLabel }}</span>
-        </button>
+        </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/LayoutView.vue` around lines 599 - 609, The
info chip is rendered as a focusable <button> despite having no activation
behavior; change the element to a non-interactive container (e.g., replace the
<button v-else-if="cell.kind === 'action-info'"> with a <div> or <span> using
the same class "info-cell") and remove the button-only attributes such as
type="button"; keep informational attributes like :title="infoTitle" and the
child bindings (infoDims, infoLabel) and either remove aria-label or replace it
with a non-interactive accessibility attribute (e.g., aria-hidden="false" or
role="status") as appropriate so the markup is non-focusable while preserving
the information.

383-394: ⚠️ Potential issue | 🟡 Minor

Handle failed image loads explicitly.

If the image fails to load, dimensions stays indistinguishable from “still loading.” Add onerror before assigning src.

🐛 Proposed fix
     img.onload = () => {
       // Guard against race: if the URL changed before load, skip.
       if (selectedHistory.value?.output.url !== url) return
       dimensions.value = { w: img.naturalWidth, h: img.naturalHeight }
     }
+    img.onerror = () => {
+      if (selectedHistory.value?.output.url !== url) return
+      dimensions.value = null
+    }
     img.src = url
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/LayoutView.vue` around lines 383 - 394, The
image load watcher currently only sets dimensions on img.onload and leaves
dimensions as null on failure which is indistinguishable from "loading"; add an
img.onerror handler before assigning img.src that sets dimensions to a distinct
failure sentinel (for example dimensions.value = { w: 0, h: 0 } or
dimensions.value = false) and keep the same race guard comparing
selectedHistory.value?.output.url to url inside onerror as you do in onload so
stale errors are ignored; update the watcher around selectedHistory, dimensions,
img, onload and onerror accordingly.

335-366: ⚠️ Potential issue | 🔴 Critical

Await workflow loading before rerun/reuse.

app.loadGraphData(workflow) is not awaited, and actionReuseParams() fires it without waiting. Rerun/reuse can therefore operate on the old graph; extraction failures also bypass the try in actionRerun().

🐛 Proposed fix
-async function loadSelectedWorkflow() {
+async function loadSelectedWorkflow(): Promise<boolean> {
   const sel = selectedHistory.value
-  if (!sel) return
+  if (!sel) return false
   const { workflow } = await extractWorkflowFromAsset(sel.asset)
-  if (!workflow) return
+  if (!workflow) return false
   if (workflow.id !== app.rootGraph.id) {
-    app.loadGraphData(workflow)
-    return
+    await app.loadGraphData(workflow)
+    return true
   }
-  const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker
+  const changeTracker = workflowStore.activeWorkflow?.changeTracker
   if (!changeTracker) {
-    app.loadGraphData(workflow)
-    return
+    await app.loadGraphData(workflow)
+    return true
   }
   changeTracker.redoQueue = []
   await changeTracker.updateState([workflow], changeTracker.undoQueue)
+  return true
 }
 
 async function actionRerun() {
-  await loadSelectedWorkflow()
   try {
+    const loaded = await loadSelectedWorkflow()
+    if (!loaded) return
     await commandStore.execute('Comfy.QueuePrompt', {
       metadata: { subscribe_to_run: false, trigger_source: 'linear' }
     })
   } catch (error) {
-    console.error('[LayoutView] rerun failed:', error)
+    toastErrorHandler(error)
   }
 }
 
-function actionReuseParams() {
-  void loadSelectedWorkflow()
+async function actionReuseParams() {
+  try {
+    await loadSelectedWorkflow()
+  } catch (error) {
+    toastErrorHandler(error)
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/LayoutView.vue` around lines 335 - 366,
loadSelectedWorkflow must fully await graph loading and signal success so
callers don't proceed on stale data: modify loadSelectedWorkflow to await
app.loadGraphData(workflow) (instead of fire-and-forget) and return a boolean
(true on successful load/update, false on early exit/failure); ensure
extractWorkflowFromAsset errors are not swallowed so they propagate. Then update
actionRerun and actionReuseParams to await loadSelectedWorkflow() and only
continue (call commandStore.execute or reuse logic) when it returns true; keep
the existing changeTracker.updateState await as-is. Use the function names
loadSelectedWorkflow, actionRerun, actionReuseParams, app.loadGraphData,
extractWorkflowFromAsset, and commandStore.execute to locate changes.
src/components/appMode/layout/panels/FloatingPanel.vue (3)

67-85: ⚠️ Potential issue | 🟠 Major

Avoid committing a drag on a plain header click.

usePanelDrag() enters dragging on pointerdown, so a click without movement can still commit a snap on pointerup. Gate the commit behind a moved/min-distance flag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/FloatingPanel.vue` around lines 67 - 85,
The drag logic currently starts on pointerdown and commits a snap on pointerup
even for clicks; update the drag flow to require meaningful movement before
committing: add a moved/min-distance flag (e.g., expose a didMove or hasMoved
ref from usePanelDrag or compute it inside the hook) and only call onCommit (or
emit 'update:preset') when that flag is true; update usePanelDrag (and/or the
returned onHeaderPointerDown/onPointerUp handlers) to set didMove when movement
exceeds the threshold and to skip calling the commit callback when didMove is
false; keep references to usePanelDrag, onHeaderPointerDown, isDragging and
handleHeaderPointerDown to locate the change.

214-218: ⚠️ Potential issue | 🟠 Major

Keep bottom-anchored presets anchored when collapsed.

.floating-panel--collapsed forces bottom: auto, which breaks float-br / float-bl because those presets rely on bottom for their vertical position.

🐛 Proposed fix
 .floating-panel--collapsed {
-  bottom: auto !important;
-  height: auto !important;
-  max-height: none !important;
+  height: auto;
+  max-height: none;
 }

Also applies to: 249-268

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/FloatingPanel.vue` around lines 214 -
218, The collapse rule .floating-panel--collapsed is unsetting bottom (bottom:
auto !important) which breaks bottom-anchored presets (float-br / float-bl);
remove the bottom: auto !important from the .floating-panel--collapsed CSS (and
the duplicate rule block later) so collapsing only affects height/max-height and
not the bottom positioning, preserving bottom-anchored preset behavior.

42-57: 🛠️ Refactor suggestion | 🟠 Major

Use Vue 3.5 reactive destructuring for prop defaults.

withDefaults() is still used here; switch to reactive destructuring so defaults live directly in the typed defineProps declaration.

♻️ Proposed refactor
-const props = withDefaults(
-  defineProps<{
-    preset: PanelPreset
-    collapsed?: boolean
-    title?: string
-    movable?: boolean
-    /** Preset to restore when the user picks "Reset layout". */
-    defaultPreset?: PanelPreset
-  }>(),
-  {
-    collapsed: false,
-    title: undefined,
-    movable: false,
-    defaultPreset: 'right-dock'
-  }
-)
+const {
+  preset,
+  collapsed = false,
+  title,
+  movable = false,
+  defaultPreset = 'right-dock'
+} = defineProps<{
+  preset: PanelPreset
+  collapsed?: boolean
+  title?: string
+  movable?: boolean
+  /** Preset to restore when the user picks "Reset layout". */
+  defaultPreset?: PanelPreset
+}>()

As per coding guidelines, “Do not use withDefaults or runtime props declaration in Vue 3.5+.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/FloatingPanel.vue` around lines 42 - 57,
Replace the withDefaults(...) wrapper with Vue 3.5 reactive destructuring: keep
the typed defineProps<...> declaration for PanelPreset/props but remove
withDefaults and instead destructure defaults from the returned props object
(e.g., const props = defineProps<{ preset: PanelPreset; collapsed?: boolean;
title?: string; movable?: boolean; defaultPreset?: PanelPreset }>(); const {
collapsed = false, title = undefined, movable = false, defaultPreset =
'right-dock' } = props). Update any code that uses the original props binding to
use the new props/destructured values; ensure the symbol names (props,
defineProps, collapsed, title, movable, defaultPreset, PanelPreset) are used as
shown.
🧹 Nitpick comments (1)
src/renderer/extensions/linearMode/LinearWelcome.vue (1)

16-25: Extract panelSide to a shared source to avoid drift with LayoutView.vue.

The same concept is computed in src/components/appMode/layout/LayoutView.vue (lines 161–175) using panelPreset.endsWith('-dock') + startsWith('left') for docks and endsWith('l') for floats. The implementation here enumerates only 'left-dock' | 'float-tl' | 'float-bl', which is functionally equivalent today but will silently diverge if a new left-side float preset (e.g. a mid-row float-*l) is added. Expose panelSide from useAppModeStore (or a small composable) and consume it in both places.

♻️ Sketch
// in appModeStore (or useAppMode composable)
const panelSide = computed<'left' | 'right'>(() =>
  panelPreset.value.endsWith('-dock')
    ? panelPreset.value.startsWith('left') ? 'left' : 'right'
    : panelPreset.value.endsWith('l') ? 'left' : 'right'
)

Then in LinearWelcome.vue:

-const { hasOutputs, hasNodes, panelPreset } = storeToRefs(appModeStore)
-
-// Which viewport side the panel is docked on. ...
-const panelSide = computed(() => {
-  const p = panelPreset.value
-  if (p === 'left-dock' || p === 'float-tl' || p === 'float-bl') return 'left'
-  return 'right'
-})
+const { hasOutputs, hasNodes, panelSide } = storeToRefs(appModeStore)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/extensions/linearMode/LinearWelcome.vue` around lines 16 - 25,
The computed panelSide in LinearWelcome.vue duplicates logic that exists in
LayoutView.vue and may drift; move the logic into the shared app mode store or a
small composable (e.g., export panelSide from useAppModeStore or useAppMode) and
have LinearWelcome.vue and LayoutView.vue consume that single computed instead
of their own local computation. Implement panelSide as a
computed<'left'|'right'> that uses panelPreset.value.endsWith('-dock') ?
panelPreset.value.startsWith('left') ? 'left' : 'right' :
panelPreset.value.endsWith('l') ? 'left' : 'right', then replace references to
the local panelSide in LinearWelcome.vue (and the duplicated logic in
LayoutView.vue) to use the shared panelSide from the store/composable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/appMode/layout/cells/JobQueueCell.vue`:
- Around line 32-49: The button currently uses an aria-label which overrides the
visible count text (button element with class "job-queue-cell" and `@click`
handler "handleClick"), so either remove the :aria-label attribute entirely or
replace it with an accessible reference: add an id to the visible count span
(element with class "job-queue-cell__label") and set aria-labelledby on the
button to point to that span id so assistive tech reads the visible "N active"
text; if you need extra action text, append it as visually hidden text inside
that span rather than using aria-label.

---

Duplicate comments:
In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 315-330: The computed selectedHistory currently uses id.split(':')
which breaks when asset IDs contain colons; update selectedHistory to locate the
last colon after the "history:" prefix (use lastIndexOf(':') or find the index
of the last delimiter), extract assetId as the substring between "history:" and
that last colon and parse outputIndex from the substring after that last colon,
then proceed to look up asset in outputs.media.value and select
allOutputs(asset)[outputIndex] as before to avoid mis-parsing IDs.
- Around line 599-609: The info chip is rendered as a focusable <button> despite
having no activation behavior; change the element to a non-interactive container
(e.g., replace the <button v-else-if="cell.kind === 'action-info'"> with a <div>
or <span> using the same class "info-cell") and remove the button-only
attributes such as type="button"; keep informational attributes like
:title="infoTitle" and the child bindings (infoDims, infoLabel) and either
remove aria-label or replace it with a non-interactive accessibility attribute
(e.g., aria-hidden="false" or role="status") as appropriate so the markup is
non-focusable while preserving the information.
- Around line 383-394: The image load watcher currently only sets dimensions on
img.onload and leaves dimensions as null on failure which is indistinguishable
from "loading"; add an img.onerror handler before assigning img.src that sets
dimensions to a distinct failure sentinel (for example dimensions.value = { w:
0, h: 0 } or dimensions.value = false) and keep the same race guard comparing
selectedHistory.value?.output.url to url inside onerror as you do in onload so
stale errors are ignored; update the watcher around selectedHistory, dimensions,
img, onload and onerror accordingly.
- Around line 335-366: loadSelectedWorkflow must fully await graph loading and
signal success so callers don't proceed on stale data: modify
loadSelectedWorkflow to await app.loadGraphData(workflow) (instead of
fire-and-forget) and return a boolean (true on successful load/update, false on
early exit/failure); ensure extractWorkflowFromAsset errors are not swallowed so
they propagate. Then update actionRerun and actionReuseParams to await
loadSelectedWorkflow() and only continue (call commandStore.execute or reuse
logic) when it returns true; keep the existing changeTracker.updateState await
as-is. Use the function names loadSelectedWorkflow, actionRerun,
actionReuseParams, app.loadGraphData, extractWorkflowFromAsset, and
commandStore.execute to locate changes.

In `@src/components/appMode/layout/panels/FloatingPanel.vue`:
- Around line 67-85: The drag logic currently starts on pointerdown and commits
a snap on pointerup even for clicks; update the drag flow to require meaningful
movement before committing: add a moved/min-distance flag (e.g., expose a
didMove or hasMoved ref from usePanelDrag or compute it inside the hook) and
only call onCommit (or emit 'update:preset') when that flag is true; update
usePanelDrag (and/or the returned onHeaderPointerDown/onPointerUp handlers) to
set didMove when movement exceeds the threshold and to skip calling the commit
callback when didMove is false; keep references to usePanelDrag,
onHeaderPointerDown, isDragging and handleHeaderPointerDown to locate the
change.
- Around line 214-218: The collapse rule .floating-panel--collapsed is unsetting
bottom (bottom: auto !important) which breaks bottom-anchored presets (float-br
/ float-bl); remove the bottom: auto !important from the
.floating-panel--collapsed CSS (and the duplicate rule block later) so
collapsing only affects height/max-height and not the bottom positioning,
preserving bottom-anchored preset behavior.
- Around line 42-57: Replace the withDefaults(...) wrapper with Vue 3.5 reactive
destructuring: keep the typed defineProps<...> declaration for PanelPreset/props
but remove withDefaults and instead destructure defaults from the returned props
object (e.g., const props = defineProps<{ preset: PanelPreset; collapsed?:
boolean; title?: string; movable?: boolean; defaultPreset?: PanelPreset }>();
const { collapsed = false, title = undefined, movable = false, defaultPreset =
'right-dock' } = props). Update any code that uses the original props binding to
use the new props/destructured values; ensure the symbol names (props,
defineProps, collapsed, title, movable, defaultPreset, PanelPreset) are used as
shown.

In `@src/renderer/extensions/linearMode/LinearWelcome.vue`:
- Around line 88-99: The Run button removes keyboard focus styling via the
classes outline-none and focus:outline-none, leaving keyboard users without a
visible focus state; update the button in LinearWelcome.vue (the button that
calls runFromPill) to restore an accessible focus-visible style instead of
suppressing it — remove the hard focus suppression classes and add a visible
focus-visible style (e.g., a ring, outline or box-shadow applied on
:focus-visible) so the primary action shows clear keyboard focus.
- Around line 35-44: In runFromPill, replace the bare console.error swallow with
the same error-handling pattern used in RunCell.vue: import and call
useErrorHandling().toastErrorHandler to surface failures to the user when
commandStore.execute(commandId, { metadata: { subscribe_to_run: false,
trigger_source: 'linear' } }) rejects; keep the try/catch, pass the caught error
and a clear message like "Queue prompt failed" into toastErrorHandler so queue
failures are shown to users instead of only being logged.

---

Nitpick comments:
In `@src/renderer/extensions/linearMode/LinearWelcome.vue`:
- Around line 16-25: The computed panelSide in LinearWelcome.vue duplicates
logic that exists in LayoutView.vue and may drift; move the logic into the
shared app mode store or a small composable (e.g., export panelSide from
useAppModeStore or useAppMode) and have LinearWelcome.vue and LayoutView.vue
consume that single computed instead of their own local computation. Implement
panelSide as a computed<'left'|'right'> that uses
panelPreset.value.endsWith('-dock') ? panelPreset.value.startsWith('left') ?
'left' : 'right' : panelPreset.value.endsWith('l') ? 'left' : 'right', then
replace references to the local panelSide in LinearWelcome.vue (and the
duplicated logic in LayoutView.vue) to use the shared panelSide from the
store/composable.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4beb8216-81eb-40ba-8ba3-bd6d3e5fcc56

📥 Commits

Reviewing files that changed from the base of the PR and between e28af8c and d9ae0dd.

📒 Files selected for processing (7)
  • src/components/appMode/layout/LayoutView.vue
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/components/appMode/layout/cells/IconCell.vue
  • src/components/appMode/layout/cells/JobQueueCell.vue
  • src/components/appMode/layout/panels/FloatingPanel.vue
  • src/components/appMode/layout/panels/PanelDragPreview.vue
  • src/renderer/extensions/linearMode/LinearWelcome.vue
✅ Files skipped from review due to trivial changes (2)
  • src/components/appMode/layout/cells/IconCell.vue
  • src/components/appMode/layout/panels/PanelDragPreview.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/appMode/layout/cells/BatchCountCell.vue

Comment thread src/components/appMode/layout/cells/JobQueueCell.vue
Comment thread src/assets/css/style.css
</div>
</template>

<style scoped>
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.

This looks great. How do you feel about trying to express all these styles with tailwind and design system tokens? Not that one is necessarily better than the other but just to align with other components in our system 🫡

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 299292a69. What you were looking at was placeholder, scoped styles while the UI/UX was still in flux, never meant as the final solution. All the App Mode cells dropped their <style scoped> blocks, tokens moved into packages/design-system/, and utilities like text-layout-md now come from Tailwind the same way as the rest of the design system.

Comment on lines +117 to +130
/* Widget content (textareas, inputs, dropdowns) normally renders at
text-xs via WidgetTextarea/WidgetInputNumber. Override to the
default layout scale. */
.input-cell__body :deep(textarea),
.input-cell__body :deep(input) {
font-size: var(--layout-font-md) !important;
}

/* Let multiline textareas fill the cell body. */
.input-cell__body :deep(textarea) {
height: 100% !important;
min-height: 0 !important;
resize: none !important;
}
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.

Would it be possible to use variants for the input cell and/or use props? See:

base: 'relative inline-flex cursor-pointer items-center select-none rounded-lg bg-secondary-background text-base-foreground outline-none transition-all duration-200 ease-in-out hover:bg-secondary-background-hover border-[2.5px] border-solid disabled:cursor-default disabled:opacity-30 disabled:hover:bg-secondary-background',
. Not sure if it would be better here but could be

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I held off on this for now, but I am doing a new UI/UX pass today and will look into doing this. It's noted in my planning document for the next UI/UX pass.

</span>
</div>
<div class="input-cell__body">
<DropZone>
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.

placeholder. re-review this on second pass

}
</script>

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

Here we could play aroudn with using the other components we have that display outputs and see how it feels

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 299292a69. Reused OutputHistoryItem for the thumbnail rendering instead of keeping our own image/video/icon branches. Planning to revisit this area in the UI/UX pass I'm doing today, so the exact treatment may shift again.

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.

This is a really nice setup, love it!

Comment on lines +87 to +92
const graphNodes = shallowRef<LGraphNode[]>(app.rootGraph.nodes)
useEventListener(
app.rootGraph.events,
'configured',
() => (graphNodes.value = app.rootGraph.nodes)
)
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.

This is really clean

}

const inputEntries = computed<InputEntryWithMeta[]>(() => {
void graphNodes.value
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.

If want this computed to track the graphNodes ref, we could also just replace this line with something like, if (!graphNodes.value) return [] -- which may or may not be a good thing to have in and of itself.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Kept the void graphNodes.value pattern but added a comment in 299292a69 explaining its purpose. That line exists to trigger a re-run when the graph reconfigures, so stale input selections get pruned when new nodes load or the workflow switches. Your suggestion handles a different case: returning [] when graphNodes is missing. Since graphNodes is initialized from app.rootGraph.nodes and shouldn't be falsy in practice, we kept the existing pattern and just documented it.

There's a good chance I'm confused here and don't fully understand this issue. I'm making a note to revisit it in my next UI/UX pass.

Comment on lines +235 to +297
function moveBlock(from: BlockPos, target: DropTarget) {
// Work on a fresh copy so Vue sees a new top-level value.
const rows: BlockRow[] = panelRows.value.map((r) => r.slice())
if (!rows[from.row] || rows[from.row][from.col] === undefined) return
const moved = rows[from.row][from.col]

// --- Noop detection on pre-removal grid ----------------------------
const sourceRowLen = rows[from.row].length
const isSoloRow = sourceRowLen === 1

if (
(target.kind === 'columnBefore' &&
target.rowIndex === from.row &&
target.colIndex === from.col) ||
(target.kind === 'columnAfter' &&
target.rowIndex === from.row &&
target.colIndex === from.col)
) {
return
}
if (
isSoloRow &&
(target.kind === 'newRowBefore' || target.kind === 'newRowAfter') &&
target.rowIndex === from.row
) {
return
}

// --- Remove from source --------------------------------------------
rows[from.row].splice(from.col, 1)
const sourceRowRemoved = rows[from.row].length === 0
if (sourceRowRemoved) rows.splice(from.row, 1)

// --- Insert at destination -----------------------------------------
const shiftForRemovedRow = (idx: number) =>
sourceRowRemoved && from.row < idx ? idx - 1 : idx

if (target.kind === 'newRowBefore' || target.kind === 'newRowAfter') {
const base =
target.kind === 'newRowAfter' ? target.rowIndex + 1 : target.rowIndex
const destRow = shiftForRemovedRow(base)
rows.splice(destRow, 0, [moved])
} else {
const destRow = shiftForRemovedRow(target.rowIndex)
const row = rows[destRow]
if (!row) return
let destCol = target.colIndex
// Same-row compaction: if we removed a block to the left of the
// target column within the same row, shift the target col left.
if (
!sourceRowRemoved &&
from.row === target.rowIndex &&
from.col < destCol
) {
destCol -= 1
}
if (target.kind === 'columnAfter') destCol += 1
destCol = Math.max(0, Math.min(row.length, destCol))
row.splice(destCol, 0, moved)
}

panelRows.value = rows
}
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.

nit: I would potentially move all this logic to its own module(s) and composable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right. Planning to fix this in my UI/UX pass today.

isMultiline: boolean
}

const inputEntries = computed<InputEntryWithMeta[]>(() => {
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.

If this felt a bit messy to implement, don't worry -- we are currently working on making a cleaner-to-use graphStore that will make it a lot easier.

DrJKL and others added 2 commits April 21, 2026 16:01
Responds to feedback from CodeRabbit and @christian-byrne.

Critical
- Rerunning a history item now waits for the workflow to finish
  loading before queueing, so a different-workflow rerun queues the
  correct graph instead of whatever was previously loaded.

Drag lifecycle
- Panel and block drags now use @vueuse/core for auto-cleaning
  listeners, ignore stray pointers from second fingers on touch
  devices, require a 5px movement before counting as a drag, and
  abandon cleanly if the window loses focus mid-drag.
- Collapsed floating panel no longer strips its bottom anchor, so
  bottom-docked panels stay in place when collapsed.

Error surfaces
- Queue failures (rerun, Welcome's Run pill, "reuse parameters")
  surface as toasts instead of silent console logs.
- Broken thumbnail images log a warning instead of leaving the
  dimensions pill stuck loading.

Builder-mode chrome
- Sidebar buttons no longer appear in builder mode where the panel
  can't actually open.
- Builder canvas no longer reserves a splitter column for the
  hidden node-properties panel.
- Builder menu trigger position respects whether the sidebar is on
  the left or right.
- Disabled footer in the builder surfaces its tooltip and
  not-allowed cursor on hover.

Vue house style
- LayoutGrid, FloatingPanel, and PanelBlockList migrated to Vue 3.5
  destructured defineProps + defineEmits; withDefaults and callback-
  style props dropped.

Design-system alignment
- Run button adopts the shared Button primitive's primary variant.
  The green-Run-button conversation moves to its own follow-up PR.
- App Mode tokens moved to packages/design-system/src/css/layout.css
  in Tailwind v4 @theme format; hex fallbacks dropped so themes stay
  adaptive. Layout colors bridge to PrimeVue's theme tokens so dark
  mode resolves correctly.
- Cell <style scoped> blocks converted to Tailwind utilities.

Accessibility
- \"N active\" jobs count is no longer overridden by an aria-label.
- Info chip changed from focusable-but-inert <button> to <div>.
- Output thumbnails have explicit aria-labels.

Minor
- Output thumbs delegate media rendering to OutputHistoryItem.
- Drop-indicator predicates rewritten as plain functions so they
  stop allocating a fresh computed every render.
- History-ID parser survives assetIds that contain ':'.
- nodeTitleUtil trims once up front; whitespace-only titles return
  empty.
- Dead align prop dropped from FeedbackCell.
- LayoutGrid resolvePos JSDoc clarified for the -1 edge case.

Docs
- DESIGN.md at repo root and packages/design-system/README.md seed
  the design system's discoverability for agents and contributors.

Not addressed with code
- One CodeRabbit drag-drop finding was a false positive; workflow
  file imports already work via the document-level drop handler in
  src/scripts/app.ts, regardless of which LinearView branch renders.
- A few review items were declined or deferred to follow-ups (see
  thread replies).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/components/builder/BuilderMenu.vue (1)

112-126: ⚠️ Potential issue | 🟠 Major

Mirror the builder trigger for right-sidebar layouts.

data-sidebar='right' still falls through to left: var(--spacing-layout-outer), so the trigger stays on the left edge instead of clearing the right sidebar. Add an explicit right-side rule and unset left.

Suggested fix
 .builder-menu-trigger {
   top: calc(var(--workflow-tabs-height) + var(--spacing-layout-outer));
-  /* Default position assumes sidebar-on-right → trigger flush with the
-     left viewport edge (+ outer padding). Sidebar-on-left pushes it
-     right by --sidebar-width so it clears the sidebar column. */
   left: var(--spacing-layout-outer);
   height: var(--spacing-layout-cell);
   padding: 0 12px;
   color: var(--color-base-foreground);
   background-color: var(--color-secondary-background);
   border-radius: var(--radius-layout-cell);
 }
+
 .builder-menu-trigger[data-sidebar='left'] {
   left: calc(var(--sidebar-width, 0px) + var(--spacing-layout-outer));
+  right: auto;
+}
+
+.builder-menu-trigger[data-sidebar='right'] {
+  right: calc(var(--sidebar-width, 0px) + var(--spacing-layout-outer));
+  left: auto;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/builder/BuilderMenu.vue` around lines 112 - 126, Add a CSS
rule for .builder-menu-trigger[data-sidebar='right'] that unsets the left
property and positions the trigger from the right edge by using right:
calc(var(--sidebar-width, 0px) + var(--spacing-layout-outer)); specifically,
create a selector matching .builder-menu-trigger[data-sidebar='right'] that sets
left: unset (or left: auto) and right to the calc expression so the trigger
clears the right sidebar analogous to the existing [data-sidebar='left'] rule.
🧹 Nitpick comments (4)
DESIGN.md (2)

9-23: The new layout.css isn't listed in "Where things live".

This PR introduces packages/design-system/src/css/layout.css as the source of truth for App Mode layout tokens (spacing, radii, type scale, motion, --color-layout-*), and the App Mode README explicitly directs readers to it. DESIGN.md is the entry-point map for the UI layer, but the table only mentions _palette.css, style.css, and fonts.css. Please add a row for layout.css so readers land in the right place when touching App Mode layout tokens.

📝 Proposed addition
 | Global color + font tokens  | `packages/design-system/src/css/_palette.css`                               | Adding a palette color or font scale entry      |
+| Layout tokens (App Mode)    | `packages/design-system/src/css/layout.css`                                 | Grid size, type scale, radii, motion, layout colors |
 | Base styles                 | `packages/design-system/src/css/style.css`                                  | Shared resets / baseline rules                  |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DESIGN.md` around lines 9 - 23, Update the "Where things live" table in
DESIGN.md to include a new row for the App Mode layout tokens that points to
packages/design-system/src/css/layout.css; mention its purpose (App Mode layout
tokens: spacing, radii, type scale, motion, --color-layout-*) and use case
(editing App Mode layout tokens / layout-related design decisions) so readers
are directed to layout.css (referenced as layout.css and "App Mode" in the PR)
when modifying layout tokens.

26-36: Minor: specify a language on the fenced code block.

markdownlint (MD040) flags the plain-text layered-model diagram. A simple text (or mermaid if you'd prefer a real diagram) silences the warning.

📝 Proposed fix
-```
+```text
 Figma                           (design source of truth, fetched live via MCP)
    ↓
 Tailwind v4 `@theme` tokens       (packages/design-system/src/css/_palette.css)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DESIGN.md` around lines 26 - 36, The fenced code block containing the
layered-model diagram in DESIGN.md lacks a language specifier; update that
block's opening fence from ``` to ```text (or ```mermaid if you want a real
diagram) so markdownlint MD040 is satisfied—look for the diagram block showing
"Figma → Tailwind v4 `@theme` tokens → CVA variants → Reka UI primitives → Feature
components" and change its opening fence to include the language identifier.
src/components/appMode/layout/cells/OutputThumbCell.vue (1)

55-63: Use cn() (or a single static string) instead of :class="[]".

All entries in the array are static Tailwind strings — there's no dynamic merging here. Per the repo guideline, :class="[]" should be replaced with cn() from @/utils/tailwindUtil (or, since nothing is conditional, just a plain class="...").

As per coding guidelines: "Never use :class="[]" to merge class names, always use import { cn } from '@/utils/tailwindUtil'".

♻️ Proposed refactor
   <button
     type="button"
-    :class="[
-      'm-0 flex size-full cursor-pointer overflow-hidden rounded-layout-cell border-2 border-transparent bg-layout-cell p-0',
-      'duration-layout transition-[border-color] ease-layout',
-      'hover:border-white/20',
-      'data-[active=true]:border-layout-cell-hover',
-      '[&_img]:pointer-events-none [&_img]:size-full [&_img]:object-cover',
-      '[&_video]:pointer-events-none [&_video]:size-full [&_video]:object-cover',
-      '[&_i]:m-auto [&_i]:size-3/5 [&_i]:text-layout-mute'
-    ]"
+    class="m-0 flex size-full cursor-pointer overflow-hidden rounded-layout-cell border-2 border-transparent bg-layout-cell p-0 duration-layout transition-[border-color] ease-layout hover:border-white/20 data-[active=true]:border-layout-cell-hover [&_img]:pointer-events-none [&_img]:size-full [&_img]:object-cover [&_video]:pointer-events-none [&_video]:size-full [&_video]:object-cover [&_i]:m-auto [&_i]:size-3/5 [&_i]:text-layout-mute"
     :data-active="isActive"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/cells/OutputThumbCell.vue` around lines 55 -
63, The class binding on OutputThumbCell.vue currently uses a static array in
:class — replace it with a static class string or use cn() to follow repo
guidelines: either change :class="[...] " to a plain class="m-0 flex size-full
... [&_i]:text-layout-mute" or import { cn } from '@/utils/tailwindUtil' and use
:class="cn('m-0 flex size-full ... [&_i]:text-layout-mute')" ensuring you remove
the array syntax and add the import only if you choose cn().
packages/design-system/src/css/layout.css (1)

49-64: Consider documenting App Mode's dark-only constraint for future light-theme work.

The inline comment (lines 49–58) honestly captures the design tradeoff: falling back to --p-* tokens directly fixed the "panels rendered light when chrome was dark" issue. However, --p-surface-800 / --p-surface-700 are scale values—in light mode they resolve to dark greys (zinc.800/#27272a), which would invert panel appearance if light-theme support is ever added.

Since App Mode currently has no light-mode implementation, this is not a present issue. However, to prevent future confusion, consider adding a comment clarifying that these tokens assume dark-mode only: "App Mode currently targets dark mode exclusively; if light-theme support is planned, revisit semantic tokens (--p-surface-card, --p-surface-section) and address the original cascade issues."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/design-system/src/css/layout.css` around lines 49 - 64, Add an
explicit note to the existing comment explaining that the layout CSS tokens
(--color-layout-canvas, --color-layout-cell, --color-layout-cell-hover,
--color-layout-text, --color-layout-mute) intentionally map directly to PrimeVue
scale tokens (--p-content-background, --p-surface-800, --p-surface-700,
--p-text-color, --p-text-muted-color) and therefore assume App Mode targets dark
mode only; also add a short guidance that if light-theme support is ever
introduced, maintainers should revisit these mappings (e.g., prefer semantic
tokens like --p-surface-card / --p-surface-section instead of --p-surface-800 /
--p-surface-700) and investigate the original cascade issues that prompted the
direct mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/appMode/layout/cells/InputCell.vue`:
- Around line 62-69: The Tailwind important prefixes (`!`) used in the
InputCell.vue wrapper are overriding NodeWidgets styles and must be removed:
update the <div> classes and the NodeWidgets class binding to drop all `!`
suffixes (`text-layout-md!`, `h-full!`, `min-h-0!`, `resize-none!`, repeated
`text-layout-md!`, and the arbitrary-value override in the NodeWidgets class)
and instead either (a) add a layout prop/variant to the NodeWidgets component
(e.g., `variant="compact"` or `layout="cell"`) and implement the needed style
adjustments inside NodeWidgets, or (b) extract the overrides into a single
scoped CSS class (e.g., `.input-cell-overrides`) applied to the wrapper and
define the non-important Tailwind utilities there; update NodeWidgets to honor
the new prop or scoped class so consumers no longer use `!`.

In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 119-128: The widget matching uses loose equality when comparing
widget.sourceNodeId to storeNodeId; update the comparison in the matchingWidget
predicate (inside the fullNodeData.widgets?.find callback used with vueWidget
and node.isSubgraphNode()) to use strict equality (===) and ensure types align —
either coerce widget.sourceNodeId to string or convert storeNodeId to the same
type before comparing (so the isPromotedWidgetView branch uses
widget.sourceNodeId === storeNodeId and widget.sourceWidgetName ===
vueWidget.storeName).
- Around line 556-565: The history thumb placement loop can overflow into the
right-anchored cluster because it never considers cols.value; modify the logic
around historyThumbs (the loop that pushes items with col: col + i) to cap the
number of thumbs using cols.value and a reserved right-cluster width: compute
maxThumbs = Math.min(historyThumbs.value.length, Math.max(0, cols.value - col -
reservedRightColumns)) where reservedRightColumns is a small constant (e.g.,
4–10) representing space for the right cluster, then loop i from 0 to maxThumbs
- 1 and push only those items; ensure you guard against negative values so no
thumbs are placed when space is insufficient.

In `@src/components/appMode/layout/panels/PanelBlockList.vue`:
- Around line 147-329: The scoped <style> block in PanelBlockList.vue contains
three issues: component-local CSS instead of Tailwind utilities (classes like
.panel-block-list, .panel-block-row, .panel-block__run,
.drop-indicator--row/col), multiple :deep() overrides using !important to force
NodeWidgets layout ([data-testid='node-widgets'] and [data-testid='node-widget']
rules), and a raw hex color (`#1e40ff`) for .drop-indicator; fix by removing the
ad-hoc styles and replacing structural rules with Tailwind 4 utility classes on
the component markup (use flex, gap, grid utilities, heights, padding, etc.),
add a prop/variant on the NodeWidgets component (e.g., allowCompact or
layoutVariant) to expose the grid/padding/border adjustments so you can remove
all :deep() + !important overrides in PanelBlockList.vue, and replace the
hard-coded hex with a design token/variable (e.g.,
--color-layout-drop-indicator) wired into the theme so PanelBlockList uses the
token instead of `#1e40ff`.

In `@src/components/builder/BuilderPanel.vue`:
- Around line 90-100: The footer preview is only pointer-inert but still
keyboard-focusable; update the wrapper divs that contain BatchCountCell and
RunCell (the elements with class "builder-panel-footer-row" inside the
builder-panel-footer-disabled container) to also be inert so descendants are
removed from the tab order and disabled for all events; add the inert attribute
to the outer preview container and/or the specific footer-row divs (the divs
wrapping BatchCountCell and RunCell) and ensure any reactive/static frameworks
used support inert (polyfill if necessary) so the Button in RunCell and
ScrubableNumberInput in BatchCountCell become non-focusable while previewed.

---

Duplicate comments:
In `@src/components/builder/BuilderMenu.vue`:
- Around line 112-126: Add a CSS rule for
.builder-menu-trigger[data-sidebar='right'] that unsets the left property and
positions the trigger from the right edge by using right:
calc(var(--sidebar-width, 0px) + var(--spacing-layout-outer)); specifically,
create a selector matching .builder-menu-trigger[data-sidebar='right'] that sets
left: unset (or left: auto) and right to the calc expression so the trigger
clears the right sidebar analogous to the existing [data-sidebar='left'] rule.

---

Nitpick comments:
In `@DESIGN.md`:
- Around line 9-23: Update the "Where things live" table in DESIGN.md to include
a new row for the App Mode layout tokens that points to
packages/design-system/src/css/layout.css; mention its purpose (App Mode layout
tokens: spacing, radii, type scale, motion, --color-layout-*) and use case
(editing App Mode layout tokens / layout-related design decisions) so readers
are directed to layout.css (referenced as layout.css and "App Mode" in the PR)
when modifying layout tokens.
- Around line 26-36: The fenced code block containing the layered-model diagram
in DESIGN.md lacks a language specifier; update that block's opening fence from
``` to ```text (or ```mermaid if you want a real diagram) so markdownlint MD040
is satisfied—look for the diagram block showing "Figma → Tailwind v4 `@theme`
tokens → CVA variants → Reka UI primitives → Feature components" and change its
opening fence to include the language identifier.

In `@packages/design-system/src/css/layout.css`:
- Around line 49-64: Add an explicit note to the existing comment explaining
that the layout CSS tokens (--color-layout-canvas, --color-layout-cell,
--color-layout-cell-hover, --color-layout-text, --color-layout-mute)
intentionally map directly to PrimeVue scale tokens (--p-content-background,
--p-surface-800, --p-surface-700, --p-text-color, --p-text-muted-color) and
therefore assume App Mode targets dark mode only; also add a short guidance that
if light-theme support is ever introduced, maintainers should revisit these
mappings (e.g., prefer semantic tokens like --p-surface-card /
--p-surface-section instead of --p-surface-800 / --p-surface-700) and
investigate the original cascade issues that prompted the direct mapping.

In `@src/components/appMode/layout/cells/OutputThumbCell.vue`:
- Around line 55-63: The class binding on OutputThumbCell.vue currently uses a
static array in :class — replace it with a static class string or use cn() to
follow repo guidelines: either change :class="[...] " to a plain class="m-0 flex
size-full ... [&_i]:text-layout-mute" or import { cn } from
'@/utils/tailwindUtil' and use :class="cn('m-0 flex size-full ...
[&_i]:text-layout-mute')" ensuring you remove the array syntax and add the
import only if you choose cn().
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abc7961e-7815-4225-bf8b-b6b3d8225660

📥 Commits

Reviewing files that changed from the base of the PR and between d9ae0dd and 299292a.

📒 Files selected for processing (26)
  • DESIGN.md
  • packages/design-system/README.md
  • packages/design-system/src/css/layout.css
  • packages/design-system/src/css/style.css
  • src/assets/css/style.css
  • src/components/LiteGraphCanvasSplitterOverlay.vue
  • src/components/appMode/AppModeToolbar.vue
  • src/components/appMode/layout/LayoutGrid.vue
  • src/components/appMode/layout/LayoutView.vue
  • src/components/appMode/layout/README.md
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/components/appMode/layout/cells/FeedbackCell.vue
  • src/components/appMode/layout/cells/IconCell.vue
  • src/components/appMode/layout/cells/InputCell.vue
  • src/components/appMode/layout/cells/JobQueueCell.vue
  • src/components/appMode/layout/cells/ModeToggleCell.vue
  • src/components/appMode/layout/cells/OutputThumbCell.vue
  • src/components/appMode/layout/cells/RunCell.vue
  • src/components/appMode/layout/panels/FloatingPanel.vue
  • src/components/appMode/layout/panels/PanelBlockList.vue
  • src/components/appMode/layout/panels/PanelDragPreview.vue
  • src/components/appMode/layout/panels/useBlockDrag.ts
  • src/components/appMode/layout/panels/usePanelDrag.ts
  • src/components/builder/AppModeWidgetList.vue
  • src/components/builder/BuilderMenu.vue
  • src/components/builder/BuilderPanel.vue
✅ Files skipped from review due to trivial changes (5)
  • src/components/appMode/AppModeToolbar.vue
  • packages/design-system/README.md
  • packages/design-system/src/css/style.css
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/components/appMode/layout/cells/IconCell.vue
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/components/appMode/layout/cells/JobQueueCell.vue
  • src/components/appMode/layout/cells/FeedbackCell.vue
  • src/components/builder/AppModeWidgetList.vue
  • src/components/appMode/layout/panels/PanelDragPreview.vue
  • src/components/appMode/layout/cells/RunCell.vue
  • src/components/appMode/layout/panels/usePanelDrag.ts
  • src/components/appMode/layout/panels/FloatingPanel.vue
  • src/components/appMode/layout/cells/ModeToggleCell.vue
  • src/assets/css/style.css

Comment thread src/components/appMode/layout/cells/InputCell.vue
Comment thread src/components/appMode/layout/LayoutView.vue Outdated
Comment thread src/components/appMode/layout/LayoutView.vue Outdated
Comment thread src/components/appMode/layout/panels/PanelBlockList.vue
Comment thread src/components/builder/BuilderPanel.vue Outdated
- LayoutView: strict equality + String coercion when matching promoted
  widget source node; widget.sourceNodeId may be typed as number
  while storeNodeId is always a string, so == was masking a latent
  type mismatch.
- LayoutView: cap history thumbs at MAX_HISTORY_THUMBS=6 so positive-
  indexed thumbs don't overflow into the right cluster's end-anchored
  cells on narrow viewports. Conservative constant for typical desktop
  widths; responsive cap (reading cols from LayoutGrid) is a sensible
  follow-up.
- BuilderPanel: disabled footer preview rows now use the inert
  attribute in addition to pointer-events: none. The prior pointer-
  events block was visible-only — Button inside RunCell and
  ScrubableNumberInput inside BatchCountCell were still tab-reachable
  and keyboard-activatable. inert removes descendants from the focus
  order and swallows keyboard events.
- PanelBlockList: replace hard-coded #1e40ff drop-indicator color
  with a new --color-layout-drop-indicator token in
  packages/design-system/src/css/layout.css. Uses color-mix for the
  shadow's 25% alpha so both references flow through the single
  token.

Not addressed in this commit (larger scope than a review response):
- InputCell's Tailwind ! overrides fighting NodeWidgets defaults
  (CR r3124954679). Resolving this cleanly requires a variant prop
  on NodeWidgets or a scoped override class shared across consumers
  — bigger than this PR's scope.
- PanelBlockList's scoped <style> block + :deep(NodeWidgets)
  !important overrides (CR r3124954700). Same scope argument; the
  raw-hex concern from that comment IS fixed here.

Addresses CodeRabbit comments r3124954683, r3124954695, r3124954700
(partial: hex → token), r3124954704 on Comfy-Org#11317.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/components/appMode/layout/panels/PanelBlockList.vue (1)

97-99: ⚠️ Potential issue | 🟡 Minor

Translate the block kind before interpolating it into the aria label.

block.kind still exposes programmer-facing values like input/run to assistive tech. Map it through translated labels before passing { kind } into linearMode.blockDragReorderAria.

As per coding guidelines: "Use vue-i18n in composition API for string literals, place translation entries in src/locales/en/main.json".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/panels/PanelBlockList.vue` around lines 97 -
99, The aria-label currently interpolates raw programmer-facing values via
block.kind into t('linearMode.blockDragReorderAria'); instead map block.kind to
a user-facing translated label before passing it into the interpolation (e.g.
create a computed or helper that uses vue-i18n composition API to translate
kinds like "input"/"run" to readable strings), then call
t('linearMode.blockDragReorderAria', { kind: translatedKind }); add the
corresponding kind labels to src/locales/en/main.json per guidelines.
src/components/appMode/layout/LayoutView.vue (2)

556-568: ⚠️ Potential issue | 🟡 Minor

The fixed thumb cap can still overlap the right cluster.

With builder + history actions + share + job queue visible, col can reach 10 and the right cluster can resolve as far left as column 12 on a 24-track grid; six thumbs occupy columns 10–15, so overlap remains. This still needs a responsive cap based on the actual grid track count or a reserved right-cluster width.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/LayoutView.vue` around lines 556 - 568, The
hardcoded MAX_HISTORY_THUMBS causes overlap; replace it with a responsive cap
computed from the actual grid track count and reserved right-cluster width:
obtain the current cols value from LayoutGrid (or accept cols as a
prop/composable), define a reservedCluster width (e.g., reservedCluster = 4 or
derived from visible right-cluster items like share/jobQueue), then compute
maxThumbs = Math.max(0, cols - col - reservedCluster) and use thumbCount =
Math.min(historyThumbs.value.length, maxThumbs) instead of MAX_HISTORY_THUMBS;
update any references to MAX_HISTORY_THUMBS, thumbCount, and the loop using i to
use the new computed maxThumbs logic.

346-372: ⚠️ Potential issue | 🟠 Major

Abort rerun when workflow loading fails or finds nothing.

actionRerun() still queues after loadSelectedWorkflow() returns early for missing selection/workflow, and load/extraction errors happen before the try, so they bypass toastErrorHandler. Return a success boolean and move the load inside the guarded block.

Proposed fix
-async function loadSelectedWorkflow() {
+async function loadSelectedWorkflow(): Promise<boolean> {
   const sel = selectedHistory.value
-  if (!sel) return
+  if (!sel) return false
   const { workflow } = await extractWorkflowFromAsset(sel.asset)
-  if (!workflow) return
+  if (!workflow) return false
   if (workflow.id !== app.rootGraph.id) {
     await app.loadGraphData(workflow)
-    return
+    return true
   }
-  const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker
+  const changeTracker = workflowStore.activeWorkflow?.changeTracker
   if (!changeTracker) {
     await app.loadGraphData(workflow)
-    return
+    return true
   }
   changeTracker.redoQueue = []
   await changeTracker.updateState([workflow], changeTracker.undoQueue)
+  return true
 }
 
 async function actionRerun() {
-  await loadSelectedWorkflow()
   try {
+    const loaded = await loadSelectedWorkflow()
+    if (!loaded) return
     await commandStore.execute('Comfy.QueuePrompt', {
       metadata: { subscribe_to_run: false, trigger_source: 'linear' }
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/LayoutView.vue` around lines 346 - 372,
loadSelectedWorkflow can return early or throw, but actionRerun always proceeds
to queue the command and bypasses toastErrorHandler; change loadSelectedWorkflow
to return a boolean success (true when workflow loaded/applied, false on early
return or failure), then in actionRerun call loadSelectedWorkflow inside a
try/catch, pass any caught error to toastErrorHandler, and only call
commandStore.execute('Comfy.QueuePrompt', ...) when loadSelectedWorkflow returns
true; update references to useWorkflowStore().activeWorkflow?.changeTracker and
app.loadGraphData calls inside the modified loadSelectedWorkflow as needed to
preserve behavior.
🧹 Nitpick comments (2)
src/components/builder/BuilderPanel.vue (1)

111-162: Prefer Tailwind utilities over a <style scoped> block.

Most rules here are trivially Tailwind-expressible (flex column, gap, opacity, cursor-not-allowed, select-none, pointer-events-none, bg-transparent, fixed positioning). CSS-var references can use arbitrary-value utilities (e.g. top-[var(--workflow-tabs-height)], h-[var(--spacing-layout-cell)], bg-[var(--color-secondary-background)], rounded-[var(--radius-layout-cell)]). Only the two :deep() rules have a real reason to stay — and even those could be avoided by exposing a prop on FloatingPanel (or letting the panel own its own pointer-events: auto internally) so BuilderPanel no longer reaches into the child's internals.

As per coding guidelines: "Avoid <style> blocks in Vue components, use Tailwind 4 utility classes instead".

♻️ Sketch of the migration
   <div class="builder-panel-root">
+  <div
+    class="fixed inset-x-0 bottom-0 top-[var(--workflow-tabs-height)] z-[100] pointer-events-none"
+  >
     <FloatingPanel …>
       …
       <template `#footer`>
         <div
-          class="builder-panel-footer-disabled"
+          class="flex flex-col gap-2 opacity-55 select-none cursor-not-allowed"
           :title="t('linearMode.builder.runDisabledHint')"
           aria-disabled="true"
         >
-          <div class="builder-panel-footer-row" inert>
+          <div
+            class="h-[var(--spacing-layout-cell)] rounded-[var(--radius-layout-cell)] bg-[var(--color-secondary-background)] pointer-events-none"
+            inert
+          >
             <BatchCountCell />
           </div>
-          <div class="builder-panel-footer-row builder-panel-footer-run" inert>
+          <div
+            class="h-[var(--spacing-layout-cell)] rounded-[var(--radius-layout-cell)] bg-transparent pointer-events-none"
+            inert
+          >
             <RunCell />
           </div>
         </div>
       </template>
     </FloatingPanel>
   </div>

The two :deep() rules for .floating-panel / .panel-drag-preview are the only styles that resist a clean Tailwind migration; consider pushing the pointer-events: auto into FloatingPanel itself so this scoped block can be dropped entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/builder/BuilderPanel.vue` around lines 111 - 162, Replace the
scoped CSS in BuilderPanel.vue by moving the layout and visual rules onto the
template as Tailwind utility classes (convert .builder-panel-root to fixed with
top-[var(--workflow-tabs-height)] left-0 right-0 bottom-0 pointer-events-none
z-[100], apply flex/column gap-2 opacity-55 select-none cursor-not-allowed to
the footer via footer-disabled utilities, convert footer-row to
h-[var(--spacing-layout-cell)] bg-[var(--color-secondary-background)]
rounded-[var(--radius-layout-cell)] pointer-events-none, and set
builder-panel-footer-run to bg-transparent) using Tailwind arbitrary-value
utilities for the CSS vars; remove the <style scoped> entirely and instead
address the two :deep(...) rules by updating the child FloatingPanel (or its
public prop/class) to set pointer-events:auto on .floating-panel and
.panel-drag-preview so BuilderPanel no longer pierces into child internals.
src/components/appMode/layout/LayoutView.vue (1)

676-763: Move the component styling toward Tailwind utilities/tokens.

This new component still carries a large scoped style block with deep layout overrides. Consider moving static layout styles into Tailwind classes and promoting reusable chrome border/radius/color rules into semantic utilities or design tokens.

As per coding guidelines: "Avoid <style> blocks in Vue components, use Tailwind 4 utility classes instead".

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 405-411: The console.warn in the img.onerror handler currently
prints the raw asset URL (variable url); change it to avoid logging secrets by
removing the url from the message and instead emit a generic, non-sensitive log
like "[LayoutView] failed to load image for dimensions" or include only a
non-sensitive identifier if available; update the img.onerror block (the handler
tied to img.onerror and the selectedHistory.value check) so dimensions remain
null but no raw URL or token is written to the console.
- Around line 188-238: The panel reconciliation never seeds the initial run
controls, so when panelRows is empty the PanelBlockList never sees the
RunCell/BatchCountCell; after computing preserved (in the watchEffect that
references panelRows and inputEntries) ensure you insert the run block row when
starting from an empty panel: detect panelRows.value originally empty (or
preserved.length === 0) and unshift/prepend a row containing the run-control
blocks expected by PanelBlockList (the RunCell/BatchCountCell representation
used elsewhere as a BlockRow/BlockConfig), then proceed to append input rows as
now done.

In `@src/components/appMode/layout/panels/PanelBlockList.vue`:
- Around line 94-100: The drag handle button (.panel-block__grip) is focusable
but only listens to `@pointerdown` on startDrag, so keyboard users cannot start or
perform reordering; add a keyboard path by handling keydown on the button
(listen for Enter/Space to invoke startDrag with the same args and implement
arrow-key handling during the drag lifecycle or forward to the existing drag
logic), or if you can't implement keyboard reordering yet, remove the
interactive role/replace the element with a non-focusable span and add
aria-hidden to avoid exposing an unusable control; update handlers around
startDrag and any drag lifecycle methods so keyboard-initiated drags reuse the
same code paths.

---

Duplicate comments:
In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 556-568: The hardcoded MAX_HISTORY_THUMBS causes overlap; replace
it with a responsive cap computed from the actual grid track count and reserved
right-cluster width: obtain the current cols value from LayoutGrid (or accept
cols as a prop/composable), define a reservedCluster width (e.g.,
reservedCluster = 4 or derived from visible right-cluster items like
share/jobQueue), then compute maxThumbs = Math.max(0, cols - col -
reservedCluster) and use thumbCount = Math.min(historyThumbs.value.length,
maxThumbs) instead of MAX_HISTORY_THUMBS; update any references to
MAX_HISTORY_THUMBS, thumbCount, and the loop using i to use the new computed
maxThumbs logic.
- Around line 346-372: loadSelectedWorkflow can return early or throw, but
actionRerun always proceeds to queue the command and bypasses toastErrorHandler;
change loadSelectedWorkflow to return a boolean success (true when workflow
loaded/applied, false on early return or failure), then in actionRerun call
loadSelectedWorkflow inside a try/catch, pass any caught error to
toastErrorHandler, and only call commandStore.execute('Comfy.QueuePrompt', ...)
when loadSelectedWorkflow returns true; update references to
useWorkflowStore().activeWorkflow?.changeTracker and app.loadGraphData calls
inside the modified loadSelectedWorkflow as needed to preserve behavior.

In `@src/components/appMode/layout/panels/PanelBlockList.vue`:
- Around line 97-99: The aria-label currently interpolates raw programmer-facing
values via block.kind into t('linearMode.blockDragReorderAria'); instead map
block.kind to a user-facing translated label before passing it into the
interpolation (e.g. create a computed or helper that uses vue-i18n composition
API to translate kinds like "input"/"run" to readable strings), then call
t('linearMode.blockDragReorderAria', { kind: translatedKind }); add the
corresponding kind labels to src/locales/en/main.json per guidelines.

---

Nitpick comments:
In `@src/components/builder/BuilderPanel.vue`:
- Around line 111-162: Replace the scoped CSS in BuilderPanel.vue by moving the
layout and visual rules onto the template as Tailwind utility classes (convert
.builder-panel-root to fixed with top-[var(--workflow-tabs-height)] left-0
right-0 bottom-0 pointer-events-none z-[100], apply flex/column gap-2 opacity-55
select-none cursor-not-allowed to the footer via footer-disabled utilities,
convert footer-row to h-[var(--spacing-layout-cell)]
bg-[var(--color-secondary-background)] rounded-[var(--radius-layout-cell)]
pointer-events-none, and set builder-panel-footer-run to bg-transparent) using
Tailwind arbitrary-value utilities for the CSS vars; remove the <style scoped>
entirely and instead address the two :deep(...) rules by updating the child
FloatingPanel (or its public prop/class) to set pointer-events:auto on
.floating-panel and .panel-drag-preview so BuilderPanel no longer pierces into
child internals.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bb4dd9d8-28e1-4eb1-bcd8-fbf891775ae0

📥 Commits

Reviewing files that changed from the base of the PR and between 299292a and d436f54.

📒 Files selected for processing (4)
  • packages/design-system/src/css/layout.css
  • src/components/appMode/layout/LayoutView.vue
  • src/components/appMode/layout/panels/PanelBlockList.vue
  • src/components/builder/BuilderPanel.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/design-system/src/css/layout.css

Comment on lines +188 to +238
watchEffect(() => {
const desiredInputIds = new Set(
inputEntries.value.map((e) => `input-${e.key}`)
)
const entryByBlockId = new Map(
inputEntries.value.map((e) => [`input-${e.key}`, e])
)
const existingInputIds = new Set<string>()
for (const row of panelRows.value) {
for (const b of row) {
if (b.kind === 'input') existingInputIds.add(b.id)
}
}

// Preserve rows; drop blocks whose input entry is gone; refresh meta.
const preserved: BlockRow[] = panelRows.value
.map(
(row): BlockRow =>
row.flatMap((block): BlockConfig[] => {
if (block.kind === 'input') {
if (!desiredInputIds.has(block.id)) return []
const entry = entryByBlockId.get(block.id)!
return [
{
...block,
entryKey: entry.key,
isMultiline: entry.isMultiline
}
]
}
return [block]
})
)
.filter((row) => row.length > 0)

// Append brand-new inputs as single-block rows at the end.
for (const entry of inputEntries.value) {
const id = `input-${entry.key}`
if (existingInputIds.has(id)) continue
preserved.push([
{
id,
kind: 'input',
entryKey: entry.key,
isMultiline: entry.isMultiline
}
])
}

panelRows.value = preserved
})
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Seed the panel with the run block.

panelRows starts empty and this reconciliation only appends input blocks, so the RunCell / BatchCountCell branch in PanelBlockList is unreachable from the initial state. The floating panel will show inputs but no panel run controls.

Proposed fix
   // Append brand-new inputs as single-block rows at the end.
   for (const entry of inputEntries.value) {
     const id = `input-${entry.key}`
     if (existingInputIds.has(id)) continue
@@
       }
     ])
   }
 
+  if (!preserved.some((row) => row.some((block) => block.kind === 'run'))) {
+    preserved.push([
+      {
+        id: 'run',
+        kind: 'run',
+        withBatchCount: true
+      }
+    ])
+  }
+
   panelRows.value = preserved
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/appMode/layout/LayoutView.vue` around lines 188 - 238, The
panel reconciliation never seeds the initial run controls, so when panelRows is
empty the PanelBlockList never sees the RunCell/BatchCountCell; after computing
preserved (in the watchEffect that references panelRows and inputEntries) ensure
you insert the run block row when starting from an empty panel: detect
panelRows.value originally empty (or preserved.length === 0) and unshift/prepend
a row containing the run-control blocks expected by PanelBlockList (the
RunCell/BatchCountCell representation used elsewhere as a BlockRow/BlockConfig),
then proceed to append input rows as now done.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 385b611a0 by removing the vestigial code rather than seeding it. A prior commit (d9ae0dd85 "hoist Run cluster out of panel") moved the Run + BatchCount cells out of the floating panel and onto the main grid's right cluster (the system-run / system-batch-count / system-job-queue cells in LayoutView's cells computed). The run block handling inside PanelBlockList was leftover from before that move — never seeded because it's not meant to be in the panel anymore. 385b611a0 removed the RunBlock type variant, the template branch, the RunCell / BatchCountCell / useI18n imports, and the scoped .panel-block__run* CSS. knip confirms nothing else referenced it.

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.

@eliheuer, thanks for the detailed explanation — that's a much cleaner resolution. Removing the dead RunBlock branch is the right call; seeding it would have reintroduced coupling that the d9ae0dd85 hoist was specifically meant to eliminate. Marking this as resolved.


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/LayoutView.vue:188-238
Timestamp: 2026-04-22T17:54:40.349Z
Learning: In `src/components/appMode/layout/LayoutView.vue` and `src/components/appMode/layout/panels/PanelBlockList.vue`, the Run and BatchCount cells are intentionally placed on the main LayoutGrid's right cluster (`system-run`, `system-batch-count`, `system-job-queue` cells in `LayoutView`'s `cells` computed), NOT inside the floating `FloatingPanel` / `PanelBlockList`. Commit `d9ae0dd85` ("hoist Run cluster out of panel") established this architecture; commit `385b611a0` removed the vestigial `RunBlock` type, `PanelBlockList` template branch, `RunCell`/`BatchCountCell`/`useI18n` imports, and `.panel-block__run*` scoped CSS that were left over from a prior design. Do not flag the absence of a `run`-kind block seed in the `panelRows` watchEffect as a bug — it is intentional.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/views/LinearView.vue:121-151
Timestamp: 2026-04-22T17:12:51.213Z
Learning: In `src/views/LinearView.vue`, the `dragDrop` / `drop` binding on the center splitter panel in the old `LinearView` branch was NOT responsible for workflow file imports (.json/.png). Workflow file imports are handled at the document level by `app.addDropHandler()` in `src/scripts/app.ts` via `useEventListener(document, 'drop', ...)`, which fires regardless of which branch renders. The `dragDrop` relay only routed per-widget drops to `LinearControls.handleDragDrop` → `AppModeWidgetList.handleDragDrop`. In the new `showLayoutView` branch, per-widget drops work through per-cell `<DropZone>` wrappers in `InputCell.vue`. Do not flag the absence of `drop="dragDrop"` in the new App Mode layout branch as a regression.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/LayoutView.vue:556-575
Timestamp: 2026-04-22T17:45:08.551Z
Learning: In `src/components/appMode/layout/LayoutView.vue`, the history thumbnail column placement uses a hard-coded `MAX_HISTORY_THUMBS = 6` cap (commit d436f549f) as a pragmatic guard against collision with the right-anchored cluster (system-run, system-batch-count, etc.). A proper responsive cap—using `cols.value` from `LayoutGrid` via `defineExpose`—is intentionally deferred because the entire chrome layout is being rethought in a future UI/UX pass. Do not flag the static cap or the absence of a `cols.value` guard as issues in this file.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/PanelBlockList.vue:130-294
Timestamp: 2026-04-22T17:49:23.393Z
Learning: In `src/components/appMode/layout/panels/PanelBlockList.vue`, the scoped `<style>` block (instead of Tailwind utilities) and the `:deep()` overrides using `!important` on `[data-testid='node-widgets']` and `[data-testid='node-widget']` grid/padding/border rules are intentional and known tech-debt. The proper fix is to add a `variant` or `layoutVariant` prop to `NodeWidgets` so layout consumers don't need to override its defaults from the outside. That refactor touches `NodeWidgets` and all its consumers and is deferred to a follow-up PR. The drop-indicator color (`#1e40ff`) was replaced with the `--color-layout-drop-indicator` design-system token in commit d436f549f. Do not re-flag the scoped style block or the `!important` `:deep()` NodeWidgets overrides in `PanelBlockList.vue`.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/InputCell.vue:62-69
Timestamp: 2026-04-22T17:39:54.735Z
Learning: In `src/components/appMode/layout/cells/InputCell.vue`, the Tailwind `!` important-suffix overrides on the body wrapper div (e.g., `[&_textarea]:h-full!`, `[&_textarea]:min-h-0!`, `[&_textarea]:resize-none!`, `[&_input]:text-layout-md!`) and on the `NodeWidgets` class binding are intentional and known tech-debt. The proper fix is to add a `variant="cell"` (or equivalent) prop to `NodeWidgets` so layout consumers don't need to shout over its defaults. That refactor touches `NodeWidgets` and all its consumers and is deferred to a follow-up PR. Do not re-flag these `!` overrides in `InputCell.vue`.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/PanelBlockList.vue:0-0
Timestamp: 2026-04-22T17:03:35.095Z
Learning: In `src/components/appMode/layout/panels/PanelBlockList.vue`, the block-drag grip element was intentionally demoted from a `<button>` to a `<span aria-hidden="true">` (commit 385b611a0) to remove it from the accessibility tree pending a proper keyboard-reorder implementation. The `linearMode.blockDragReorderAria` i18n key is kept in `src/locales/en/main.json` as a forward-compatible placeholder for when keyboard reorder lands as a follow-up. Do not flag the aria-hidden grip or the unused translation key as issues.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/builder/BuilderPanel.vue:90-94
Timestamp: 2026-04-22T17:32:04.712Z
Learning: In `src/components/builder/BuilderPanel.vue`, the disabled builder footer preview (Run + BatchCount cells) uses the `inert` HTML attribute on the inner `.builder-panel-footer-row` divs to fully remove them from the tab order, assistive-technology focus, and all pointer events, while keeping the outer wrapper div interactive so that the native `title` tooltip and `cursor: not-allowed` remain discoverable on hover. Do not flag the `inert` usage on these inner rows as unnecessary — it is required because `pointer-events: none` alone does not prevent keyboard users from focusing interactive descendants.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-20T01:57:38.157Z
Learning: Before implementing user-facing features, consult the Comfy Design Standards Figma file

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: henrikvilhelmberglund
Repo: Comfy-Org/ComfyUI_frontend PR: 7617
File: src/components/actionbar/ComfyActionbar.vue:301-308
Timestamp: 2025-12-18T16:03:02.066Z
Learning: In the ComfyUI frontend queue system, useQueuePendingTaskCountStore().count indicates the number of tasks in the queue, where count = 1 means a single active/running task and count > 1 means there are pending tasks in addition to the active task. Therefore, in src/components/actionbar/ComfyActionbar.vue, enable the 'Clear Pending Tasks' button only when count > 1 to avoid clearing the currently running task. The active task should be canceled using the 'Cancel current run' button instead. This rule should be enforced via a conditional check on the queue count, with appropriate disabled/aria-disabled states for accessibility, and tests should verify behavior for count = 1 and count > 1.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:2-89
Timestamp: 2026-02-07T14:46:21.179Z
Learning: In Vue components (e.g., TreeExplorerV2.vue and similar Recka UI components) avoid wrapping ContextMenuRoot with a dynamic <component :is="..."> and rendering alternative branches. Use v-if / v-else to render separate ContextMenuRoot and non-ContextMenuRoot branches to ensure correct behavior with Reka UI, even if it duplicates some tree structure. This guideline applies to Vue components under src/components and can be checked by ensuring there is a dedicated render path for the ContextMenuRoot without dynamic component switching.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:3-5
Timestamp: 2026-02-08T16:04:58.463Z
Learning: In TreeExplorerV2.vue and similar Re- ka UI Tree components, avoid defensive copies in bindings like :expanded="[...expandedKeys]". Use v-model:expanded="expandedKeys" instead. TreeRoot follows Vue's controlled component pattern and emits update:expanded with the new array; it does not mutate the passed array. Defensive copies break reactivity and can desynchronize state. Ensure components rely on update:expanded and do not mutate input arrays.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 8761
File: src/components/dialog/content/MissingModelsWarning.vue:21-27
Timestamp: 2026-02-10T00:58:07.904Z
Learning: In the ComfyUI_frontend Vue codebase, replace raw <button> HTML elements with the shared Button component located at src/components/ui/button/Button.vue. Import and use it with appropriate variants (e.g., variant="link") to align with the design system. Apply this pattern across Vue components under src/components, ensuring consistent styling and behavior instead of ad-hoc button markup.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 9511
File: src/components/common/WorkflowActionsDropdown.vue:53-60
Timestamp: 2026-03-06T22:05:25.071Z
Learning: When using PrimeVue's v-tooltip in Vue components, be aware of a known bug where the tooltip centers on the trigger. If positioning requires a left-anchored placement, it may be acceptable and necessary to apply a deliberate workaround (e.g., adjust root transform with translateX(calc(50% - 16px)) and reposition the tooltip arrow with left utilities) rather than flagging as fragile or using !important. This pattern may appear in multiple Vue components under src/components/ that use v-tooltip; apply the same left-anchoring adjustment where appropriate and document the rationale so future maintainers understand the intentional workaround.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10135
File: src/components/rightSidePanel/errors/TabErrors.vue:288-295
Timestamp: 2026-03-17T07:02:53.709Z
Learning: In TabErrors.vue, ensure the guard used for the singleRuntimeErrorGroup relies on some((e) => e.isRuntimeError) to detect a runtime error among the runtime/validation error cards. Do not switch to every() as that would be overly restrictive given the backend’s mutual exclusion: a run either yields a runtime error or a set of validation errors. This guidance applies to Vue components in the src/components directory (generalized to all .vue files under src/components).

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment thread src/components/appMode/layout/LayoutView.vue Outdated
Comment thread src/components/appMode/layout/panels/PanelBlockList.vue Outdated
- LayoutView img.onerror: drop the URL from the console.warn message.
  Asset URLs may carry signed tokens; logging them is a soft leak.
  Message now just identifies which watcher failed; dimensions still
  reset to null via the watch's top-level assignment.
- PanelBlockList grip: demote from focusable <button> to non-focusable
  <span aria-hidden="true">. The grip only responds to pointerdown;
  a keyboard user tabbing to it could hit Enter and get no response
  — the focusable-but-inert antipattern. Hiding from the tab order
  and assistive tech matches reality until a keyboard reorder path
  (Enter/Space to grab, arrow keys to move) lands as a follow-up.
- PanelBlockList: remove the vestigial 'run' block kind. A previous
  commit (d9ae0dd "hoist Run cluster out of panel") moved the
  Run button to the grid's right cluster as a `system-run` cell;
  the in-panel `run` block handling (template branch + RunCell/
  BatchCountCell imports + scoped .panel-block__run* CSS + the
  RunBlock variant of BlockConfig) has been dead code since. Also
  drops the now-unused useI18n/t import since the grip's aria-label
  was its only template consumer.

Addresses CodeRabbit comments r3125117056, r3125117064, r3125117073
on Comfy-Org#11317.
Share the grid-anchored chrome between App Mode and the builder via a
new AppChrome.vue with a variant prop. App Mode cells emit from the
same logic in both views; builder variant hides contextually-wrong
cells (mode toggle, builder icon) and disables runtime cells (Run,
BatchCount) via declarative sets. Adding a new App Mode cell now
surfaces in the builder by construction.

Panel block state (panelRows + reconciliation + moveBlock) moves into
appModeStore + a useAppPanelLayout composable so the 2D layout is the
same state across views. BuilderPanel renders PanelBlockList directly,
replacing the AppModeWidgetList + DraggableList wrapper.

InputCell gains a variant prop. Builder variant inerts the widget body
and makes the header label double-click editable via EditableText —
zero layout delta vs App Mode when not editing, replacing the ⋯
Popover with an invisible-until-needed rename flow. AppInput's
selection checkbox is suppressed inside the panel via a new
HideInputSelectionKey injection; pick/deselect stays on the graph
canvas.

BuilderBackdrop renders the App Mode dot grid + LinearPreview in the
arrange step so the Preview matches runtime. LinearView no longer
mounts in builder mode, so the old splitter-based arrange UI doesn't
run in parallel. SideToolbar stays visible across builder steps
(removed the !isBuilderMode guard in GraphCanvas). BuilderMenu swapped
to layout tokens that bridge to PrimeVue so it reads dark.
Collapse the distributed-gap CSS Grid (LayoutGrid) into fixed-gutter
flex zones pinned to the top-left, top-right, and bottom-left corners
of AppChrome. Every chrome cell's width composes from the same cell +
gutter tokens FloatingPanel uses (`span × cell + (span-1) × gutter`),
so chrome and panel snap to identical pixel positions at every
viewport — no more per-viewport drift between the Run cluster and the
panel's left edge.

LayoutGrid.vue deleted (only consumer was AppChrome). AppChrome's cell
model collapses from {col, row, colSpan, rowSpan} grid coordinates to
{id, kind, span} + a zone. No visual change — same cells, same
placements, same variant behavior (HIDE_IN_BUILDER / DISABLE_IN_BUILDER
sets).

Unify outer-margin spacing: --spacing-layout-outer 16 → 8 so
edge-to-cell matches cell-to-cell (--spacing-layout-gutter = 8).
Token-first path: BuilderToolbar top and BuilderFooterToolbar bottom
swapped from ad-hoc Tailwind values to the layout token;
LayoutGrid's numeric outerPadding prop replaced by `p-layout-outer`
(auto-generated from the @theme token).

Add --panel-dock-width = calc(8 * cell + 7 * gutter) = 440px, derived
from grid tokens so panel width follows cell/gutter changes
automatically.

BuilderBackdrop renders dot-grid + LinearPreview in arrange. LinearView
no longer mounts in builder mode. SideToolbar now visible in builder
(removed the !isBuilderMode guard in GraphCanvas, offset AppChrome past
sidebar-width). BuilderMenu swapped to layout tokens so it reads dark.
Canvas info overlay (T:/I:/Comfy Cloud) hidden in builder via an
appModeStore watcher on isBuilderMode.
eliheuer added 2 commits May 6, 2026 19:08
- outputWindowStore.clear() now resets nextSeq alongside nextZ; the
  two counters were drifting between sessions.
- Expose outputWindowStore's `windows` as a readonly computed
  rather than the mutable underlying ref. Internal mutation
  continues via the closure. Per typescript.md (don't expose
  internal implementation types).
- Tighten LayoutView.vue's <Transition> style-block exception
  comment to the same in-block format used by ModeToggleCell,
  LatentPreview, and LinearPreview, including a note that
  panel-enter-enter is intentional (Vue appends its own -enter
  suffix to the named transition).
- Hoist the PanelPreset enum in workflowSchema.ts into a
  zPanelPreset constant with `satisfies z.ZodType<PanelPreset>`,
  mirroring the zRendererType pattern. Prevents silent drift if
  the source union in panelTypes.ts gains a member without the
  schema following.

Typecheck clean. No behavior change.
Earlier in this branch we added --color-success-foreground,
--color-destructive-foreground, --color-primary-foreground to the
@theme block in packages/design-system/src/css/style.css alongside the
existing *-background siblings. That made the corresponding
text-*-foreground utility classes resolvable for the first time.

Several files across the codebase have used those classes since
2026-03-23 (Hunter, PRs unrelated to this branch). Their text was
silently inert prior — the classes were unknown to Tailwind and
rendered no color, so the better-tailwindcss/enforce-consistent-class-order
lint rule passed by ignoring them. With the tokens now live, the rule
fires and demands the canonical order.

Pure mechanical reorder via `pnpm exec eslint --fix`. No semantic
change. The sites in question (PricingTable, TeamWorkspacesDialog,
maskeditor settings panels, queue notification, search-type filter)
will now actually render their intended white text on saturated
backgrounds — flag in case the visual change is unexpected.
eliheuer added 2 commits May 6, 2026 21:08
Earlier in this branch the linearData sync watcher started writing a
`layout: { panelRows: [...] }` block alongside `inputs` and
`outputs`. Three exact-equality assertions in appModeStore.test.ts
predated that change and continued to expect the old two-key shape.

Update them to assert the new shape (`layout: { panelRows: [] }` for
the empty-default case). 29/29 tests pass.
Address the test gap surfaced by the deep audit:

src/stores/appModeStore.test.ts adds an 11-test `describe('viewport')`
block covering the new pan/zoom math:

- zoomStep clamps to MIN_SCALE on extreme zoom-out and to MAX_SCALE on
  extreme zoom-in (the load-bearing 0.1×–8× bounds).
- zoomAt anchors the focal pixel under the cursor across a zoom step
  (tested via the workspace<->client coordinate round-trip).
- panBy accumulates offsets.
- resetView zeros offsets and resets scale to 1 from any prior state.
- toggleNoZoomMode enables noZoom and resets the viewport in one step;
  toggling back off does not re-reset.
- All viewport actions (panBy, zoomStep, zoomAt, flyTo) are no-ops
  when noZoomMode is on.

src/renderer/extensions/linearMode/outputWindowStore.test.ts is a new
12-test suite covering the public store API:

- upsert spawns with monotonic zIndex/createdSeq, deduplicates by id,
  prunes past MAX_TILES only when noZoomMode is on, and prunes
  finalized tiles before in-flight ones (the eviction priority that
  protects streaming generations).
- move/resize/attachAspect/remove behave as documented; attachAspect
  rejects non-finite and non-positive values.
- promote raises a window above all others; is a no-op when already
  topmost (caps zIndex inflation).
- clear drops windows AND resets nextZ/nextSeq counters, which fixes
  the prior counter-drift between sessions.
- sortedWindows returns by zIndex ascending.

Conforms to docs/testing/store-testing.md: createTestingPinia +
setActivePinia + vi.clearAllMocks per beforeEach. The
appModeStore dependency is mocked via vi.hoisted so outputWindowStore
tests don't pull in the full appModeStore graph (canvasStore,
settingStore, useAppMode, etc.).
@eliheuer
Copy link
Copy Markdown
Author

eliheuer commented May 7, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/platform/cloud/subscription/components/PricingTable.vue (1)

104-111: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

text-primary-foreground (white) has insufficient contrast in light mode on bg-muted-foreground/30

The parent <p> element (line 101) has bg-muted-foreground/30, which resolves to charcoal-200 (#494a50) at 30% opacity in light mode — approximately rgb(200, 201, 202), a very light gray. The button uses text-primary-foreground (white), resulting in a contrast ratio of ~1.7:1, failing WCAG AA accessibility requirements (minimum 4.5:1).

Additionally, text-primary-foreground is semantically intended for text on primary-background (blue) surfaces, not muted ones. The hover state already changes to text-base-foreground, confirming white is problematic in this context.

Use a token with sufficient contrast for light mode (e.g., text-base-foreground or another darker foreground token appropriate for muted surfaces).

🤖 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 `@src/platform/cloud/subscription/components/PricingTable.vue` around lines 104
- 111, In PricingTable.vue update the button inside the <p> with
bg-muted-foreground/30 to use a darker foreground token (e.g., replace the
text-primary-foreground class on the button with text-base-foreground or another
darker foreground token) so the contrast against bg-muted-foreground/30 meets
WCAG AA; keep the existing hover/focus behavior (hover already switches to
text-base-foreground) and ensure the emitted event emit('chooseTeamWorkspace')
remains unchanged.
src/stores/appModeStore.test.ts (1)

356-365: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard temporary app.rootGraph override with try/finally.

If this test throws before restoration, global mocked state leaks into later tests and can cause false failures. Wrap the override/restore in try/finally.

Suggested fix
-      const originalRootGraph = app.rootGraph
-      Object.defineProperty(app, 'rootGraph', { value: null, writable: true })
-
-      store.selectedOutputs.push(1)
-      await nextTick()
-
-      Object.defineProperty(app, 'rootGraph', {
-        value: originalRootGraph,
-        writable: true
-      })
+      const originalRootGraph = app.rootGraph
+      Object.defineProperty(app, 'rootGraph', { value: null, writable: true })
+      try {
+        store.selectedOutputs.push(1)
+        await nextTick()
+      } finally {
+        Object.defineProperty(app, 'rootGraph', {
+          value: originalRootGraph,
+          writable: true
+        })
+      }
🤖 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 `@src/stores/appModeStore.test.ts` around lines 356 - 365, The test temporarily
sets app.rootGraph to null but restores it only after awaiting nextTick, so if
an error occurs the mocked global state may leak; wrap the override and the
actions that depend on it (the Object.defineProperty that sets app.rootGraph to
null, the mutation store.selectedOutputs.push(1), and the await nextTick()) in a
try/finally and restore app.rootGraph to originalRootGraph in the finally block
using the same Object.defineProperty call so restoration always runs even if the
test throws.
src/renderer/extensions/linearMode/LinearPreview.vue (1)

113-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Surface workflow-load failures on rerun/reuse.

This path now depends on extractWorkflowFromAsset(), app.loadGraphData(), and changeTracker.updateState(), but failures here are not handled locally. A rejected load leaves “Reuse parameters” / “Rerun” without an actionable error from this screen. Catch the failure, show the existing toast/error UI, and abort the rerun when the workflow could not be loaded.

As per coding guidelines: src/**/*.{vue,ts}: “Implement proper error handling” and src/**/*.{js,ts,vue}: “Provide user-friendly and actionable error messages”.

🤖 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 `@src/renderer/extensions/linearMode/LinearPreview.vue` around lines 113 - 129,
The rerun path currently lets failures in loadWorkflow propagate silently;
update loadWorkflow and rerun to catch and handle errors from
extractWorkflowFromAsset, app.loadGraphData, and changeTracker.updateState: wrap
the await calls in try/catch inside loadWorkflow, return a boolean success flag
(or throw a handled Error) and when rerun calls loadWorkflow check the result
and abort if loading failed; on failure show the existing toast/error UI with a
clear actionable message (include workflow id/name and the caught error message)
and do not call runButtonClick when loadWorkflow fails. Use the symbols
loadWorkflow, rerun, extractWorkflowFromAsset, app.loadGraphData,
changeTracker.updateState, runButtonClick, and selectedItem to locate and modify
the logic.
src/stores/appModeStore.ts (1)

220-231: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset panel layout defaults before applying workflow data.

If the next workflow has no extra.linearData.layout (or only a partial one), loadSelections() leaves the previous workflow's panelPreset, panelCollapsed, panelWidthCells, and panelRows in place. That leaks stale layout state across workflows and can preserve old non-input blocks until the user manually fixes the layout.

🛠️ Suggested fix
 function loadSelections(data: Partial<LinearData> | undefined) {
   const { inputs, outputs } = pruneLinearData(data)
   selectedInputs.value = inputs
   selectedOutputs.value = outputs

+  panelPreset.value = 'right-dock'
+  panelCollapsed.value = false
+  panelWidthCells.value = 8
+  panelRows.value = []
+
   const layout = data?.layout
   if (!layout) return
-  if (layout.panelPreset) panelPreset.value = layout.panelPreset
+  if (layout.panelPreset) panelPreset.value = layout.panelPreset
   if (typeof layout.panelCollapsed === 'boolean')
     panelCollapsed.value = layout.panelCollapsed
-  if (layout.panelWidthCells) panelWidthCells.value = layout.panelWidthCells
-  if (layout.panelRows) panelRows.value = layout.panelRows
+  if (layout.panelWidthCells) panelWidthCells.value = layout.panelWidthCells
+  if (layout.panelRows) panelRows.value = layout.panelRows
 }
🤖 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 `@src/stores/appModeStore.ts` around lines 220 - 231, loadSelections currently
preserves previous panel layout values when data.layout is missing or partial;
reset the panel layout defaults first (panelPreset, panelCollapsed,
panelWidthCells, panelRows) before applying any values from data.layout so stale
values don't leak across workflows. In loadSelections, initialize those four
reactive values to their default/initial values (the same defaults used on store
creation) before checking data?.layout, then only overwrite them when layout
fields are present (e.g., if (layout.panelPreset) ...), ensuring
undefined/missing fields revert to defaults rather than keeping prior state.
♻️ Duplicate comments (5)
src/platform/workspace/components/PricingTableWorkspace.vue (1)

163-185: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Same invisible-in-light-mode issue: text-success-foreground (white) on white card background

This is the same regression as in PricingTable.vue — feature-row check icons using text-success-foreground (resolves to white) rendered directly on the card surface (bg-base-background = white in light mode). The fix is the same: replace text-success-foreground with text-success-background to use the green semantic color as an icon color on a neutral surface.

🤖 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 `@src/platform/workspace/components/PricingTableWorkspace.vue` around lines 163
- 185, The check icons in PricingTableWorkspace.vue (the three <i> elements used
in the feature rows, including the conditional one that checks tier.customLoRAs)
use the utility class text-success-foreground (white) and become invisible on
light cards; change those icon class usages from text-success-foreground to
text-success-background so the check icons use the green semantic color on a
neutral surface (update every occurrence in the feature rows including the
conditional v-if/v-else block).
src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue (1)

74-95: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Same invisible-in-light-mode issue: text-success-foreground (white) on neutral background

The three feature-availability check icons in the expandable features section have the same regression: text-success-foreground (white) on a neutral background surface, invisible in light mode. Apply text-success-background instead, consistent with the fix needed in PricingTable.vue and PricingTableWorkspace.vue.

🤖 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 `@src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue`
around lines 74 - 95, In SubscriptionAddPaymentPreviewWorkspace.vue replace the
three check icon classes that use text-success-foreground with
text-success-background so the icons are visible in light mode: update the two
static check icons in the GPU and addCredits rows and the conditional check icon
for hasCustomLoRAs (the <i> elements rendering the success check) to use
text-success-background instead of text-success-foreground; keep the negative
icon (pi-times) unchanged.
src/components/appMode/layout/PanelHeader.vue (1)

16-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-tighten the chevron label props.

expandLabel and collapseLabel are optional again, while collapsible still defaults to true and the toggle is icon-only. That reopens the path where this component renders an unnamed control if a caller forgets one of those props.

♿ Suggested fix
 } = defineProps<{
   title?: string
   draggable?: boolean
   dragging?: boolean
   collapsible?: boolean
   menuEntries: MenuItem[]
-  /** Required when `collapsible` is true so icon-only buttons have accessible names. */
-  expandLabel?: string
-  collapseLabel?: string
+  expandLabel: string
+  collapseLabel: string
   menuLabel: string
 }>()

Based on learnings, "If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action."

Also applies to: 56-63

🤖 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 `@src/components/appMode/layout/PanelHeader.vue` around lines 16 - 25, The
component allows an icon-only toggle to render without accessible text because
expandLabel and collapseLabel are optional while collapsible defaults to true;
update the props and runtime checks so that expandLabel and collapseLabel are
always provided when collapsible is true: make expandLabel and collapseLabel
required in the defineProps signature (or supply sensible defaults) and add a
runtime assertion in the PanelHeader setup/render code that throws or logs a
clear error if collapsible is true but either label is missing, and ensure the
toggle button uses the chosen prop for its aria-label; reference the defineProps
block, the collapsible/expandLabel/collapseLabel props and the toggle button
renderer to locate where to change types and add the check.
src/assets/css/style.css (1)

113-128: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore a keyboard-only focus outline here.

These rules clear the native focus affordance and suppress the wrapper ring, but the current file no longer adds the compensating :focus-visible outline. Keyboard users can lose track of the active control inside the floating panel.

🛠️ Suggested fix
 .app-mode-themed textarea:focus,
 .app-mode-themed input:focus,
 .app-mode-themed .bg-component-node-widget-background:focus-within {
   border-color: var(--fg-color);
   outline: none;
   box-shadow: none;
 }
+
+.app-mode-themed textarea:focus-visible,
+.app-mode-themed input:focus-visible,
+.app-mode-themed .bg-component-node-widget-background:has(:focus-visible) {
+  outline: 2px solid rgb(255 255 255 / 0.7);
+  outline-offset: 1px;
+}

Based on learnings, "App Mode CSS guideline: In .floating-panel and .app-chrome, mouse focus intentionally uses a subtle border-color change, while keyboard focus must show a visible 2px outline via :focus-visible."

🤖 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 `@src/assets/css/style.css` around lines 113 - 128, The current CSS removes
native focus affordances but doesn't add a keyboard-only replacement; restore a
visible keyboard focus by adding :focus-visible rules for the same controls
(e.g. .app-mode-themed textarea:focus-visible, .app-mode-themed
input:focus-visible, .app-mode-themed
.bg-component-node-widget-background:focus-within:focus-visible and any
[class*='focus-within']:focus-within:focus-visible) to show a 2px visible
outline (use outline or a 2px box-shadow with var(--fg-color)) while keeping
mouse focus as the subtle border-color change; also ensure
.floating-panel:focus-visible and .app-chrome:focus-visible follow the same 2px
outline guideline so keyboard users can track active controls.
src/components/appMode/layout/LayoutView.vue (1)

170-191: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard drag handlers by pointer ID and abort on capture failure.

The window-level listeners process all pointers, and dragging is set true even when setPointerCapture throws. A second pointer (or failed capture path) can still pan/end the active drag incorrectly.

🛠️ Suggested fix
 useEventListener(window, 'pointermove', (e: PointerEvent) => {
   if (!dragStart) return
+  if (e.pointerId !== dragStart.pointerId) return
   if (!dragging) {
     const dx = e.clientX - dragStart.x
     const dy = e.clientY - dragStart.y
     if (dx * dx + dy * dy < DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) return
     try {
       bgRef.value?.setPointerCapture(dragStart.pointerId)
+      dragging = true
     } catch {
-      // Some browsers reject capture on non-primary pointers.
+      endDrag()
+      return
     }
-    dragging = true
   }
   appModeStore.panBy(e.movementX, e.movementY)
 })
 
 function endDrag() {
   dragStart = null
   dragging = false
 }
-useEventListener(window, 'pointerup', endDrag)
-useEventListener(window, 'pointercancel', endDrag)
+useEventListener(window, 'pointerup', (e: PointerEvent) => {
+  if (!dragStart || e.pointerId !== dragStart.pointerId) return
+  endDrag()
+})
+useEventListener(window, 'pointercancel', (e: PointerEvent) => {
+  if (!dragStart || e.pointerId !== dragStart.pointerId) return
+  endDrag()
+})
🤖 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 `@src/components/appMode/layout/LayoutView.vue` around lines 170 - 191, The
window-level pointer handlers must be guarded by pointer id and must not set
dragging when setPointerCapture fails; update the pointermove listener to first
check that dragStart exists and e.pointerId === dragStart.pointerId, and when
initiating drag (in the block using
bgRef.value?.setPointerCapture(dragStart.pointerId)) catch failures and abort
(do not set dragging = true and clear dragStart or set a captureFailed flag) so
subsequent pointermoves/other pointers cannot pan; also change endDrag to accept
the PointerEvent (or pointerId) and only clear dragStart/dragging when the
ending event's pointerId matches dragStart.pointerId (and handle captureFailed
path) so useEventListener(window, 'pointerup'/'pointercancel') invoke this
guarded endDrag.
🧹 Nitpick comments (4)
src/renderer/extensions/linearMode/SelectionChrome.vue (1)

16-31: 💤 Low value

Selection overlay is mouse-only; consider keyboard equivalent later.

The teleported overlay only responds to pointer events and isn't focusable, so keyboard users can't toggle a selection. For a builder-only overlay this may be acceptable, but worth tracking as an a11y follow-up (e.g., a tabindex + Enter/Space handler, or wiring the toggle via keyboard shortcut at a higher layer).

🤖 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 `@src/renderer/extensions/linearMode/SelectionChrome.vue` around lines 16 - 31,
The overlay rendered by Teleport (the div with event handlers that emits
'toggle' on `@pointerdown.capture.stop.prevent`) is currently mouse/pointer-only
and not keyboard-focusable; make it accessible by adding focusability and
keyboard handling: give the element a tabindex (e.g., tabindex="0"), a semantic
role if appropriate (role="button"), and a keydown handler that triggers the
same toggle emit on Enter/Space (the same logic used in the `@pointerdown` ->
$emit('toggle') path), and ensure you preserve the existing pointer event
captures (Teleport and the div with
`@pointerdown/`@click/@pointerup/@pointermove/@contextmenu). This will allow
keyboard users to focus the Teleport overlay and toggle it via keyboard while
keeping existing pointer behavior intact.
src/renderer/extensions/linearMode/LinearWelcome.test.ts (1)

46-95: ⚡ Quick win

Add a test for the new welcome “Run” click path.

The mocks for @/stores/commandStore and @/composables/useErrorHandling are scaffolding for the new behavior in LinearWelcome.vue (clicking Run dispatches commandStore.execute, with shift-key selecting Comfy.QueuePromptFront), but no it(...) covers it. Per the testing guideline ("Write tests for all changes, especially bug fixes to catch future regressions"), please assert that path so a regression in the command id or the shift-key branch is caught.

♻️ Sketch
const commandExecute = vi.hoisted(() => vi.fn())
vi.mock('@/stores/commandStore', () => ({
  useCommandStore: () => ({ execute: commandExecute })
}))

it('clicking Run dispatches Comfy.QueuePrompt', async () => {
  const user = userEvent.setup()
  renderComponent({ hasNodes: true, hasOutputs: true })
  await user.click(screen.getByTestId('linear-welcome-run'))
  expect(commandExecute).toHaveBeenCalledWith('Comfy.QueuePrompt', expect.any(Object))
})

it('shift-clicking Run dispatches Comfy.QueuePromptFront', async () => {
  const user = userEvent.setup()
  renderComponent({ hasNodes: true, hasOutputs: true })
  await user.keyboard('{Shift>}')
  await user.click(screen.getByTestId('linear-welcome-run'))
  await user.keyboard('{/Shift}')
  expect(commandExecute).toHaveBeenCalledWith('Comfy.QueuePromptFront', expect.any(Object))
})

(Adjust the testid / render preconditions to whatever makes the Run button visible in LinearWelcome.vue.)

As per coding guidelines: "Write tests for all changes, especially bug fixes to catch future regressions".

🤖 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 `@src/renderer/extensions/linearMode/LinearWelcome.test.ts` around lines 46 -
95, Tests are missing for the Run button behavior: add two tests in
LinearWelcome.test.ts that render with hasNodes: true and hasOutputs: true,
simulate a normal click on the element with test id 'linear-welcome-run' and
assert useCommandStore().execute was called with 'Comfy.QueuePrompt' and any
payload, and simulate a Shift+click (or keyboard shift down, click, shift up)
and assert execute was called with 'Comfy.QueuePromptFront'; ensure the mocked
useCommandStore.execute (hoisted vi.fn()) is used so you can assert calls to it.
src/components/appMode/layout/cells/InputCell.vue (1)

100-117: ⚡ Quick win

Remove conflicting textarea resize rules.

Line 100 enables vertical resize, but Line 113-117 force resize: none, so one of these is always misleading. Keep a single source of truth.

Suggested cleanup
-          class="gap-y-3 rounded-lg py-1 [&_textarea]:resize-y **:[.col-span-2]:grid-cols-1 not-md:**:[.h-7]:h-10"
+          class="gap-y-3 rounded-lg py-1 **:[.col-span-2]:grid-cols-1 not-md:**:[.h-7]:h-10"
🤖 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 `@src/components/appMode/layout/cells/InputCell.vue` around lines 100 - 117,
The stylesheet contains conflicting textarea resize rules: the template's
utility class "[&_textarea]:resize-y" enables vertical resizing while the scoped
CSS block ".input-cell__body :deep(textarea)" sets "resize: none !important";
remove the "resize: none !important" declaration from the ".input-cell__body
:deep(textarea)" rule in InputCell.vue so the utility class controls textarea
resizing and there is a single source of truth.
src/components/appMode/layout/panels/panelTypes.ts (1)

15-22: ⚡ Quick win

Prefer function declarations for exported pure helpers.

These helpers use arrow function expressions where declarations are more idiomatic. Convert them to function declarations to align with the repository's TS convention for exported utilities.

Proposed refactor
-export const isDockPreset = (p: PanelPreset): boolean =>
-  p === 'right-dock' || p === 'left-dock'
+export function isDockPreset(p: PanelPreset): boolean {
+  return p === 'right-dock' || p === 'left-dock'
+}

-export const isFloatBottom = (p: PanelPreset): boolean =>
-  p === 'float-bl' || p === 'float-br'
+export function isFloatBottom(p: PanelPreset): boolean {
+  return p === 'float-bl' || p === 'float-br'
+}

-export const panelSide = (p: PanelPreset): 'left' | 'right' =>
-  p === 'left-dock' || p === 'float-tl' || p === 'float-bl' ? 'left' : 'right'
+export function panelSide(p: PanelPreset): 'left' | 'right' {
+  return p === 'left-dock' || p === 'float-tl' || p === 'float-bl'
+    ? 'left'
+    : 'right'
+}

Per coding guidelines: "Do not use function expressions if it's possible to use function declarations instead".

🤖 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 `@src/components/appMode/layout/panels/panelTypes.ts` around lines 15 - 22,
Change the three exported arrow helpers into named function declarations:
replace the exported const arrow functions isDockPreset, isFloatBottom, and
panelSide (which accept a PanelPreset) with equivalent exported function
declarations using the same names and signatures; ensure return types remain
boolean for isDockPreset/isFloatBottom and 'left'|'right' for panelSide,
preserve the same conditional logic, and export them so existing imports
continue to work.
🤖 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 `@browser_tests/tests/builderSaveFlow.spec.ts`:
- Around line 19-29: The reSaveAs function currently calls
appMode.footer.nav.page(), which is invalid because nav is a Locator; replace
that call with the Page instance (use appMode.footer.page if exposed, or
appMode.page / the same private accessor pattern used elsewhere) so page is a
real Playwright Page; update the function to obtain page from the correct
property and then continue using page.getByRole(...) and
appMode.saveAs.fillAndSave(workflowName, viewType) as before.

In `@src/components/appMode/layout/cells/IconCell.vue`:
- Around line 28-39: The template currently always sets :aria-label="label" even
when inlineLabel renders visible text; update the IconCell.vue template to only
bind aria-label when the component is icon-only (i.e., when inlineLabel is false
or falsy). Locate the attributes on the root button element (where
:aria-label="label", :title="label", :disabled="disabled",
`@click`="emit('activate')") and change the aria binding so it is conditional
based on the inlineLabel prop/variable (keep title and other props unchanged) so
visible text provides the accessible name and aria-label is used only for
icon-only mode.

In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 153-157: The handler handleWheel currently preventsDefault for
every wheel event and routes all to appModeStore.zoomAt, which blocks panel
scrolling; change it to only intercept wheel events that originate from the
workspace layer and otherwise let the event bubble normally. Concretely, inside
handleWheel check the event target (e.target) or use closest(...) to verify the
event came from the workspace container (e.g., an element with a workspace-layer
CSS class or data attribute) and only then call appModeStore.zoomAt and
e.preventDefault(); for events inside floating panels or other scrollable
regions return early so their native scrolling still works. Ensure you reference
the existing handleWheel function and the appModeStore.zoomAt call when making
the change.

In `@src/components/appMode/layout/OutputWindow.vue`:
- Around line 63-68: The manual drag/resize paths must be no-opped when
noZoomMode is true: update the header element to remove/disable
draggable/drag-handle attributes when the noZoomMode prop is set, do not attach
pointer-down/drag handlers (e.g., the onPointerDown/onHeaderPointerDown or
startDrag callbacks) when noZoomMode is true, and do not render or mount the
resize hit-zones/resize-handle elements in that mode; additionally, guard the
methods that emit update:position and update:size so they return early if
noZoomMode is active to prevent persisting user-driven layout changes.

In `@src/components/appMode/layout/OutputWindowList.vue`:
- Around line 118-148: loadAssetWorkflow currently returns early when
extractWorkflowFromAsset yields no workflow, allowing rerunWindow and
reuseParams to proceed erroneously; change loadAssetWorkflow to treat "no
workflow" as a failed load by throwing an Error (or rejecting) when
extractWorkflowFromAsset returns no workflow or when workflow.id mismatches and
app.loadGraphData hasn't completed, and ensure callers (rerunWindow,
reuseParams) keep their try/catch to stop execution on that thrown error;
reference functions: loadAssetWorkflow, extractWorkflowFromAsset, rerunWindow,
reuseParams, and app.loadGraphData.

In `@src/components/appMode/layout/panels/PanelDragPreview.vue`:
- Around line 22-27: sizeStyle's computed currently applies props.panelWidth
even for docked panels, which overrides the dock CSS width; modify the computed
used in PanelDragPreview.vue (symbol: sizeStyle) to skip setting style.width
when isDocked.value is true (similar to the existing height check), i.e. only
assign style.width = `${props.panelWidth}px` when props.panelWidth != null AND
!isDocked.value, leaving docked panels to use their CSS variable-based width.

In `@src/components/appMode/layout/panels/useAppPanelLayout.ts`:
- Around line 29-35: The configured handler currently reassigns the same array
reference back into the shallowRef (graphNodes), so Vue won't detect changes
when LiteGraph mutates the nodes in place; update the initial shallowRef
assignment and the useEventListener callback to replace the array with a new
copy (e.g., create a new array from app.rootGraph?.nodes) so graphNodes.value
changes identity and reliably retriggers consumers like inputEntries; modify
graphNodes, the shallowRef initialization and the 'configured' callback inside
useEventListener to assign a new array rather than the original reference.

In `@src/components/appMode/layout/PreviewCard.vue`:
- Around line 7-18: The section lacks a named landmark; update PreviewCard.vue
so the root <section> is a named landmark by adding aria-labelledby (or
aria-label fallback) that points to the title span: give the title span a stable
id (e.g., computed/prop-driven id like previewCardTitleId) and set the section's
aria-labelledby to that id (and if title can be empty, provide aria-label
fallback on the section using the title prop or a default string). Ensure the
unique id is generated/declared in the component (e.g., a computed property or
data field) and referenced from the section and title span.

In `@src/components/builder/BuilderBackdrop.vue`:
- Around line 95-118: The backdrop is currently non-interactive
(pointer-events-none) so only the transformed element (ref bgRef / class
builder-backdrop__workspace) receives handleWheel and handlePointerDown; move
those event handlers to a full-viewport hit target so panning/zooming works when
the workspace is translated: either remove pointer-events-none from the root
backdrop (the element conditioned by isArrangeMode) and attach `@wheel` and
`@pointerdown` there, or add a new full-viewport child (inside the same root) that
sits behind the transformed workspace, has pointer events enabled, and wires up
handleWheel and handlePointerDown; keep workspaceTransform and bgRef usage
unchanged and ensure dragstart.prevent remains on the interactive element.

In `@src/platform/cloud/subscription/components/PricingTable.vue`:
- Around line 149-157: Three check icon instances in PricingTable.vue (the <i
class="pi pi-check ..."> elements in the GPU row, the Add Credits row, and the
v-if="tier.customLoRAs" branch) use text-success-foreground (white) and become
invisible in light mode; change those icon class tokens to use
text-success-background (the green foreground) instead so the checkmarks are
visible on the neutral card surface, and apply this same replacement for all
three occurrences.

In `@src/renderer/extensions/linearMode/LinearPreview.vue`:
- Around line 72-76: formatEta currently returns hardcoded "m"/"s" strings which
bypasses vue-i18n; update the formatEta(s: number) function to build its output
via vue-i18n (use the i18n instance or useI18n in the component) by translating
minutes/seconds placeholders and/or using pluralization keys, then pass the
localized string into OutputWindowList instead of the raw English text;
reference the formatEta function and OutputWindowList prop where ETA is provided
and replace the hardcoded template with i18n.t or i18n.tc calls (using
placeholders like `{m}`/`{s}`) so all locales render correctly.

In `@src/renderer/extensions/linearMode/outputWindowStore.ts`:
- Around line 92-100: The eviction uses mutable zIndex (via promote()) causing
promoted older images to outrank newer finalized tiles; change evictionScore
(used by pruneToCapacity and related pruning at lines ~503-509) to use the
immutable createdSeq for ordering within each tier instead of w.zIndex, keeping
the in-flight boost logic (EVICTION_INFLIGHT_BOOST) and state check; ensure
evictionScore returns tier + w.createdSeq (or a monotonic transform so higher =
kept longer) so oldest-created windows are evicted first while in-flight entries
still outrank finalized ones.

In `@src/utils/widgetSubtitleUtil.ts`:
- Around line 38-57: TYPE_LABELS and NAME_PATTERNS are returning hardcoded
English UI strings; replace those literal values with i18n keys (e.g.,
'widget.label.text', 'widget.label.number', 'widget.name.size', etc.) and update
the mapping constants (TYPE_LABELS and NAME_PATTERNS) to return keys only; then
resolve those keys where subtitles are rendered by using vue-i18n's t() in the
composition API (or accept a Translator param into the util) and add the
corresponding entries to src/locales/en/main.json so the labels are localizable.

---

Outside diff comments:
In `@src/platform/cloud/subscription/components/PricingTable.vue`:
- Around line 104-111: In PricingTable.vue update the button inside the <p> with
bg-muted-foreground/30 to use a darker foreground token (e.g., replace the
text-primary-foreground class on the button with text-base-foreground or another
darker foreground token) so the contrast against bg-muted-foreground/30 meets
WCAG AA; keep the existing hover/focus behavior (hover already switches to
text-base-foreground) and ensure the emitted event emit('chooseTeamWorkspace')
remains unchanged.

In `@src/renderer/extensions/linearMode/LinearPreview.vue`:
- Around line 113-129: The rerun path currently lets failures in loadWorkflow
propagate silently; update loadWorkflow and rerun to catch and handle errors
from extractWorkflowFromAsset, app.loadGraphData, and changeTracker.updateState:
wrap the await calls in try/catch inside loadWorkflow, return a boolean success
flag (or throw a handled Error) and when rerun calls loadWorkflow check the
result and abort if loading failed; on failure show the existing toast/error UI
with a clear actionable message (include workflow id/name and the caught error
message) and do not call runButtonClick when loadWorkflow fails. Use the symbols
loadWorkflow, rerun, extractWorkflowFromAsset, app.loadGraphData,
changeTracker.updateState, runButtonClick, and selectedItem to locate and modify
the logic.

In `@src/stores/appModeStore.test.ts`:
- Around line 356-365: The test temporarily sets app.rootGraph to null but
restores it only after awaiting nextTick, so if an error occurs the mocked
global state may leak; wrap the override and the actions that depend on it (the
Object.defineProperty that sets app.rootGraph to null, the mutation
store.selectedOutputs.push(1), and the await nextTick()) in a try/finally and
restore app.rootGraph to originalRootGraph in the finally block using the same
Object.defineProperty call so restoration always runs even if the test throws.

In `@src/stores/appModeStore.ts`:
- Around line 220-231: loadSelections currently preserves previous panel layout
values when data.layout is missing or partial; reset the panel layout defaults
first (panelPreset, panelCollapsed, panelWidthCells, panelRows) before applying
any values from data.layout so stale values don't leak across workflows. In
loadSelections, initialize those four reactive values to their default/initial
values (the same defaults used on store creation) before checking data?.layout,
then only overwrite them when layout fields are present (e.g., if
(layout.panelPreset) ...), ensuring undefined/missing fields revert to defaults
rather than keeping prior state.

---

Duplicate comments:
In `@src/assets/css/style.css`:
- Around line 113-128: The current CSS removes native focus affordances but
doesn't add a keyboard-only replacement; restore a visible keyboard focus by
adding :focus-visible rules for the same controls (e.g. .app-mode-themed
textarea:focus-visible, .app-mode-themed input:focus-visible, .app-mode-themed
.bg-component-node-widget-background:focus-within:focus-visible and any
[class*='focus-within']:focus-within:focus-visible) to show a 2px visible
outline (use outline or a 2px box-shadow with var(--fg-color)) while keeping
mouse focus as the subtle border-color change; also ensure
.floating-panel:focus-visible and .app-chrome:focus-visible follow the same 2px
outline guideline so keyboard users can track active controls.

In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 170-191: The window-level pointer handlers must be guarded by
pointer id and must not set dragging when setPointerCapture fails; update the
pointermove listener to first check that dragStart exists and e.pointerId ===
dragStart.pointerId, and when initiating drag (in the block using
bgRef.value?.setPointerCapture(dragStart.pointerId)) catch failures and abort
(do not set dragging = true and clear dragStart or set a captureFailed flag) so
subsequent pointermoves/other pointers cannot pan; also change endDrag to accept
the PointerEvent (or pointerId) and only clear dragStart/dragging when the
ending event's pointerId matches dragStart.pointerId (and handle captureFailed
path) so useEventListener(window, 'pointerup'/'pointercancel') invoke this
guarded endDrag.

In `@src/components/appMode/layout/PanelHeader.vue`:
- Around line 16-25: The component allows an icon-only toggle to render without
accessible text because expandLabel and collapseLabel are optional while
collapsible defaults to true; update the props and runtime checks so that
expandLabel and collapseLabel are always provided when collapsible is true: make
expandLabel and collapseLabel required in the defineProps signature (or supply
sensible defaults) and add a runtime assertion in the PanelHeader setup/render
code that throws or logs a clear error if collapsible is true but either label
is missing, and ensure the toggle button uses the chosen prop for its
aria-label; reference the defineProps block, the
collapsible/expandLabel/collapseLabel props and the toggle button renderer to
locate where to change types and add the check.

In `@src/platform/workspace/components/PricingTableWorkspace.vue`:
- Around line 163-185: The check icons in PricingTableWorkspace.vue (the three
<i> elements used in the feature rows, including the conditional one that checks
tier.customLoRAs) use the utility class text-success-foreground (white) and
become invisible on light cards; change those icon class usages from
text-success-foreground to text-success-background so the check icons use the
green semantic color on a neutral surface (update every occurrence in the
feature rows including the conditional v-if/v-else block).

In
`@src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue`:
- Around line 74-95: In SubscriptionAddPaymentPreviewWorkspace.vue replace the
three check icon classes that use text-success-foreground with
text-success-background so the icons are visible in light mode: update the two
static check icons in the GPU and addCredits rows and the conditional check icon
for hasCustomLoRAs (the <i> elements rendering the success check) to use
text-success-background instead of text-success-foreground; keep the negative
icon (pi-times) unchanged.

---

Nitpick comments:
In `@src/components/appMode/layout/cells/InputCell.vue`:
- Around line 100-117: The stylesheet contains conflicting textarea resize
rules: the template's utility class "[&_textarea]:resize-y" enables vertical
resizing while the scoped CSS block ".input-cell__body :deep(textarea)" sets
"resize: none !important"; remove the "resize: none !important" declaration from
the ".input-cell__body :deep(textarea)" rule in InputCell.vue so the utility
class controls textarea resizing and there is a single source of truth.

In `@src/components/appMode/layout/panels/panelTypes.ts`:
- Around line 15-22: Change the three exported arrow helpers into named function
declarations: replace the exported const arrow functions isDockPreset,
isFloatBottom, and panelSide (which accept a PanelPreset) with equivalent
exported function declarations using the same names and signatures; ensure
return types remain boolean for isDockPreset/isFloatBottom and 'left'|'right'
for panelSide, preserve the same conditional logic, and export them so existing
imports continue to work.

In `@src/renderer/extensions/linearMode/LinearWelcome.test.ts`:
- Around line 46-95: Tests are missing for the Run button behavior: add two
tests in LinearWelcome.test.ts that render with hasNodes: true and hasOutputs:
true, simulate a normal click on the element with test id 'linear-welcome-run'
and assert useCommandStore().execute was called with 'Comfy.QueuePrompt' and any
payload, and simulate a Shift+click (or keyboard shift down, click, shift up)
and assert execute was called with 'Comfy.QueuePromptFront'; ensure the mocked
useCommandStore.execute (hoisted vi.fn()) is used so you can assert calls to it.

In `@src/renderer/extensions/linearMode/SelectionChrome.vue`:
- Around line 16-31: The overlay rendered by Teleport (the div with event
handlers that emits 'toggle' on `@pointerdown.capture.stop.prevent`) is currently
mouse/pointer-only and not keyboard-focusable; make it accessible by adding
focusability and keyboard handling: give the element a tabindex (e.g.,
tabindex="0"), a semantic role if appropriate (role="button"), and a keydown
handler that triggers the same toggle emit on Enter/Space (the same logic used
in the `@pointerdown` -> $emit('toggle') path), and ensure you preserve the
existing pointer event captures (Teleport and the div with
`@pointerdown/`@click/@pointerup/@pointermove/@contextmenu). This will allow
keyboard users to focus the Teleport overlay and toggle it via keyboard while
keeping existing pointer behavior intact.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb8f8eba-f971-4195-95d9-71379c6bc2e1

📥 Commits

Reviewing files that changed from the base of the PR and between d3ab2be and fd75632.

📒 Files selected for processing (82)
  • browser_tests/fixtures/helpers/BuilderFooterHelper.ts
  • browser_tests/fixtures/selectors.ts
  • browser_tests/tests/builderSaveFlow.spec.ts
  • packages/design-system/src/css/style.css
  • src/assets/css/style.css
  • src/components/LiteGraphCanvasSplitterOverlay.vue
  • src/components/appMode/AppModeToolbar.vue
  • src/components/appMode/layout/AppChrome.vue
  • src/components/appMode/layout/LayoutView.vue
  • src/components/appMode/layout/OutputWindow.vue
  • src/components/appMode/layout/OutputWindowList.vue
  • src/components/appMode/layout/PanelHeader.vue
  • src/components/appMode/layout/PreviewCard.vue
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/components/appMode/layout/cells/FeedbackCell.vue
  • src/components/appMode/layout/cells/IconCell.vue
  • src/components/appMode/layout/cells/InputCell.vue
  • src/components/appMode/layout/cells/JobQueueCell.vue
  • src/components/appMode/layout/cells/ModeToggleCell.vue
  • src/components/appMode/layout/cells/RunCell.vue
  • src/components/appMode/layout/panels/FloatingPanel.vue
  • src/components/appMode/layout/panels/PanelBlockList.vue
  • src/components/appMode/layout/panels/PanelDragPreview.vue
  • src/components/appMode/layout/panels/panelPresetClasses.ts
  • src/components/appMode/layout/panels/panelTypes.ts
  • src/components/appMode/layout/panels/useAppPanelLayout.ts
  • src/components/appMode/layout/panels/useBlockDrag.ts
  • src/components/appMode/layout/panels/useFlipReorder.ts
  • src/components/appMode/layout/panels/usePanelDrag.ts
  • src/components/appMode/layout/panels/usePanelResize.ts
  • src/components/appMode/layout/panels/usePointerDrag.ts
  • src/components/builder/AppBuilder.vue
  • src/components/builder/AppModeWidgetList.vue
  • src/components/builder/BuilderBackdrop.vue
  • src/components/builder/BuilderFooterToolbar.vue
  • src/components/builder/BuilderMenu.vue
  • src/components/builder/BuilderPanel.vue
  • src/components/builder/BuilderToolbar.vue
  • src/components/builder/IoItem.vue
  • src/components/builder/StepBadge.vue
  • src/components/builder/StepLabel.vue
  • src/components/builder/types.ts
  • src/components/graph/GraphCanvas.vue
  • src/components/maskeditor/BrushSettingsPanel.vue
  • src/components/maskeditor/ColorSelectSettingsPanel.vue
  • src/components/maskeditor/ImageLayerSettingsPanel.vue
  • src/components/maskeditor/PaintBucketSettingsPanel.vue
  • src/components/queue/QueueNotificationBanner.vue
  • src/components/searchbox/v2/NodeSearchTypeFilterPopover.vue
  • src/components/ui/ZoomPane.vue
  • src/locales/en/main.json
  • src/platform/cloud/subscription/components/PricingTable.vue
  • src/platform/workflow/management/stores/comfyWorkflow.ts
  • src/platform/workflow/validation/schemas/workflowSchema.test.ts
  • src/platform/workflow/validation/schemas/workflowSchema.ts
  • src/platform/workspace/components/PricingTableWorkspace.vue
  • src/platform/workspace/components/SubscriptionAddPaymentPreviewWorkspace.vue
  • src/platform/workspace/components/dialogs/TeamWorkspacesDialogContent.vue
  • src/renderer/extensions/linearMode/AppInput.vue
  • src/renderer/extensions/linearMode/AppModeWordmark.vue
  • src/renderer/extensions/linearMode/AppOutput.vue
  • src/renderer/extensions/linearMode/DragReorderDemo.vue
  • src/renderer/extensions/linearMode/ImagePreview.vue
  • src/renderer/extensions/linearMode/LatentPreview.vue
  • src/renderer/extensions/linearMode/LinearArrange.vue
  • src/renderer/extensions/linearMode/LinearPreview.vue
  • src/renderer/extensions/linearMode/LinearWelcome.test.ts
  • src/renderer/extensions/linearMode/LinearWelcome.vue
  • src/renderer/extensions/linearMode/MediaOutputPreview.vue
  • src/renderer/extensions/linearMode/SelectionChrome.vue
  • src/renderer/extensions/linearMode/VideoPreview.vue
  • src/renderer/extensions/linearMode/outputWindowStore.test.ts
  • src/renderer/extensions/linearMode/outputWindowStore.ts
  • src/renderer/extensions/linearMode/useOutputWindowSync.ts
  • src/stores/appModeStore.test.ts
  • src/stores/appModeStore.ts
  • src/types/widgetTypes.ts
  • src/utils/nodeTitleUtil.ts
  • src/utils/widgetSubtitleUtil.test.ts
  • src/utils/widgetSubtitleUtil.ts
  • src/views/GraphView.vue
  • src/views/LinearView.vue
💤 Files with no reviewable changes (5)
  • src/components/builder/types.ts
  • src/components/ui/ZoomPane.vue
  • browser_tests/fixtures/selectors.ts
  • browser_tests/fixtures/helpers/BuilderFooterHelper.ts
  • src/components/builder/IoItem.vue

Comment thread browser_tests/tests/builderSaveFlow.spec.ts
Comment thread src/components/appMode/layout/cells/IconCell.vue Outdated
Comment thread src/components/appMode/layout/LayoutView.vue
Comment thread src/components/appMode/layout/OutputWindow.vue
Comment thread src/components/appMode/layout/OutputWindowList.vue Outdated
Comment thread src/components/builder/BuilderBackdrop.vue
Comment on lines 149 to 157
<i class="pi pi-check text-xs text-success-foreground" />
</div>

<div class="flex flex-row items-center justify-between">
<span class="text-foreground text-sm font-normal">
{{ t('subscription.addCreditsLabel') }}
</span>
<i class="pi pi-check text-success-foreground text-xs" />
<i class="pi pi-check text-xs text-success-foreground" />
</div>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Invisible check icons in light mode — text-success-foreground (white) is rendered against bg-base-background (white in light mode)

--success-foreground resolves to --color-white in both themes. These feature-row icons render directly on the card surface (bg-base-background), which in light mode is also white (--color-white). The result is white-on-white — completely invisible checkmarks for GPU, Add Credits, and Custom LoRAs features in light mode.

text-success-foreground is semantically correct only when applied over a bg-success-background colored surface. For standalone indicators on a neutral card, a token like text-success-background (green) should be used instead.

🐛 Proposed fix
-              <i class="pi pi-check text-xs text-success-foreground" />
+              <i class="pi pi-check text-xs text-success-background" />

Apply the same fix to all three check icon occurrences (GPU row, Add Credits row, and the v-if="tier.customLoRAs" branch).

Also applies to: 163-166

🤖 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 `@src/platform/cloud/subscription/components/PricingTable.vue` around lines 149
- 157, Three check icon instances in PricingTable.vue (the <i class="pi pi-check
..."> elements in the GPU row, the Add Credits row, and the
v-if="tier.customLoRAs" branch) use text-success-foreground (white) and become
invisible in light mode; change those icon class tokens to use
text-success-background (the green foreground) instead so the checkmarks are
visible on the neutral card surface, and apply this same replacement for all
three occurrences.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Resolved by descoping the @theme foreground aliases in 1acdb42 + revert in a4d4145. App Mode now uses arbitrary-value syntax (text-(--success-foreground)), so the misused text-success-foreground classes in this file go back to being unknown to Tailwind — same dormant state as upstream/main HEAD, no regression introduced. Filed as upstream-PR opportunity #11 in comfyui-theme-tools for proper backfill.

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.

The PR description images show:

  • Image 1: App mode with two cyberpunk‑eye results, an “Output” panel showing step 13/30, and a right prompt panel with size fields and Run controls.
  • Image 2: A grid of “green‑eyes‑001” results; right panel shows the prompt and width/height, with Run controls.
  • Image 3: “anima‑bunny‑001” gallery and live render; right panel with prompt and size; one card shows a 23/30 progress bar.
  • Image 4: Tall mosaic of cyberpunk eyes; floating “Output” panel shows step 20/20; right panel has a taller 512×768 config.
  • Image 5: Large 1024×1024 bunny image with variants; right “Input” panel shows prompt and 1024×1024 size.

@eliheuer Thanks for the context and the commit refs. Acknowledged: with the @theme foreground aliases descoped and App Mode using text-(--success-foreground), the text-success-foreground class in PricingTable.vue is now a no-op, so the white-on-white issue won’t manifest in this PR. Given the upstream follow-up you filed, I’m good to consider this addressed here.

Optional for later: when you do the theming backfill, we can either (a) swap these to text-(--success-background) if we want explicit green checks on neutral cards, or (b) drop the color class entirely to inherit the row’s text color. Happy to open a tiny follow-up issue in this repo if you want.


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/platform/cloud/subscription/components/PricingTable.vue:0-0
Timestamp: 2026-05-07T11:23:16.180Z
Learning: Comfy-Org/ComfyUI_frontend theming (App Mode): foreground alias utilities like `text-success-foreground` are intentionally descoped and unknown to Tailwind; use arbitrary-value tokens instead (e.g., `text-(--success-foreground)`). Avoid suggesting `text-success-foreground`; prefer `text-(--success-foreground)` or an explicit semantic like `text-(--success-background)` depending on context.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/FeedbackCell.vue:0-0
Timestamp: 2026-04-28T07:02:16.613Z
Learning: In Vue SFCs (files under src/**/*.vue), avoid using Tailwind “important” modifiers (`!`, e.g., `!bg-*`, `!text-*`). For overrides that are truly needed, prefer defining/overriding styles in the component’s scoped <style> block using Vue’s `:deep()` selectors instead of relying on Tailwind `!` (e.g., follow the approach used in FeedbackCell.vue and ModeToggleCell.vue).

Comment thread src/renderer/extensions/linearMode/LinearPreview.vue Outdated
Comment thread src/renderer/extensions/linearMode/outputWindowStore.ts
Comment thread src/utils/widgetSubtitleUtil.ts Outdated
eliheuer added 11 commits May 7, 2026 01:19
…-value syntax

Earlier in this branch we exposed three new foreground tokens
(--success-foreground, --destructive-foreground, --primary-foreground)
as Tailwind utility classes by adding --color-* aliases under
@theme. That made the matching text-*-foreground / ring-*-foreground
classes resolvable globally — which inadvertently activated dormant
misuses across ~10 unrelated files (pricing tables, team dialog,
search filter, etc.) that had been silently rendering inherited
color since 2026-03-23 and now render as white text on (sometimes
white) backgrounds.

Drop the @theme aliases and switch the 5 App Mode call sites to
arbitrary-value syntax — text-(--success-foreground) and
ring-(--success-foreground)/70 instead of text-success-foreground.
Same end pixels, but the utility class names are no longer in
Tailwind's vocabulary, so the dormant misuses go back to being
inert (the same state as upstream/main HEAD).

The underlying tokens stay defined in :root and .dark-theme
because App Mode references them via var(). Net design-system
contribution from this PR: +6 lines (3 light + 3 dark).

The visual regression in the dormant-misuse files is real but
pre-existing — those sites have been broken-in-different-ways for
6+ weeks. Documented as a follow-up PR opportunity ("PR Comfy-Org#11")
in eliheuer/comfyui-theme-tools docs/upstream-pr-opportunities.md.

This commit + the next (revert of 2640d09) drop ~10 unrelated
files from this PR's diff.
Five files that exist in upstream/main fail the
better-tailwindcss/enforce-canonical-classes rule with arbitrary
sizing classes (`text-[15px]`, `rounded-[4px]`) that have token
equivalents (`text-layout-md`, `rounded-layout-cell`).

This is unrelated to the App Mode work in this PR — the rule fires
on upstream/main too. CI runs `pnpm lint:fix` and fails when the
fix produces a diff, so the autofixed shape has to land somewhere.
Including it here as a small mechanical commit.

Pure autofix via `pnpm exec eslint --fix`. No semantic change.
Eviction priority was sorted by zIndex + tier-boost. Because
`promote()` rewrites zIndex on every user click, a recently-clicked
old finalized tile would outrank newer untouched finalized tiles
during `pruneToCapacity()`, contradicting the documented
"oldest evicts first" contract.

Switch `evictionScore` to use `createdSeq` (immutable, set on
spawn) instead. Add a regression test that:
- spawns one tile,
- fills out to MAX_TILES - 1 more,
- promotes the oldest tile (lifting its zIndex to the top),
- spawns one more (triggering prune),
- asserts the promoted-old tile is the one that gets evicted.
`appMode.footer.nav.page()` was invalid: `nav` is a Playwright
`Locator` (BuilderFooterHelper.ts:7), not an object with a `.page()`
method. The call would throw at runtime; the only reason the spec
ever passed is that none of the existing tests reached the line.

Switch `reSaveAs()` to take `ComfyPage` directly and destructure
`{ page, appMode }` from it. Three call sites updated. Matches the
inline pattern used at lines 161-162 (`comfyPage.page.getByRole...`).
LiteGraph reuses the same `nodes` array reference across reconfigures
— assigning the same ref into a shallowRef is a no-op for Vue's
identity check, so the downstream `inputEntries` computed wouldn't
recompute and stale panel entries would survive until some other
reactive change incidentally retriggered it.

Spread `[...app.rootGraph?.nodes ?? []]` on both the initial value
and the `configured` handler so each assignment changes identity.
…Mode

The dashboard layout in noZoomMode auto-arranges tiles via
`relayoutDashboard()`. The component comment said tiles "can't be
moved by hand," but the pointer-drag handlers still attached, the
header still advertised itself as draggable, and the resize hit-zones
stayed mounted. Users could drag/resize tiles, breaking the grid;
the persisted update:position/update:size state survived until some
other event triggered relayout.

Gate both drag and resize on `!noZoomMode`:
- Bail in onStart handlers when noZoomMode is on (still emit
  `promote` so click still raises focus)
- :draggable="!maximized && !noZoomMode" on PanelHeader
- v-if="!maximized && !noZoomMode" on the resize hit-zone template
…workflow

`loadAssetWorkflow()` returned void and short-circuited silently
when an asset had no extractable workflow. `rerunWindow()` then
fell through to `commandStore.execute('Comfy.QueuePrompt', ...)`
which runs the currently loaded graph — meaning the user clicks
"rerun this output" and gets a run of their *current* canvas
instead.

Make loadAssetWorkflow return a boolean (true on success, false on
no-asset / no-workflow). rerunWindow early-returns on false so
Comfy.QueuePrompt only fires when a real source workflow loaded.
…alone

The @wheel handler on .layout-view caught every bubbled wheel event
and called preventDefault unconditionally — including events from
the floating panel's scrollable content. Users couldn't scroll the
panel; every scroll gesture turned into a zoom.

Gate handleWheel on whether the event target is inside bgRef (the
workspace). If not, return early and let the event bubble for native
scroll handling. Panel + chrome wheel events now behave normally;
wheel inside the workspace still zooms.
The root backdrop was `pointer-events-none`, which made the inner
transformed workspace the only hit target for @wheel and
@pointerdown. After the user panned the workspace away from an edge,
the newly-exposed dotted-grid background couldn't receive pointer
events, so a new pan/zoom gesture had to start from a non-blank part
of the workspace.

Move the handlers up to the full-viewport backdrop and drop
`pointer-events-none` from it. Pan/zoom can now start from any pixel
under the backdrop, regardless of where the workspace has been
panned to.

Note: `LayoutView`'s wheel handler still gates on bgRef.contains
(see prior commit), and BuilderBackdrop's handler doesn't need that
because there's no scrollable panel inside the backdrop.
i18n
- LinearPreview `formatEta()` now routes through vue-i18n keys
  (etaSeconds / etaMinutes / etaMinutesSeconds) so the output-window
  ETA label matches the active locale instead of always rendering
  English "5m 30s".
- widgetSubtitleUtil now returns translated strings via the global
  `t()` from `@/i18n` (matches the pattern in litegraphUtil and the
  composables). 8 new linearMode.widgetSubtitle.* keys cover the
  type labels (text, number, toggle, list, color, image) and the
  two name-pattern overrides (size, seed). Fallback for unknown
  legacy types stays as the raw lowercased type — that's a
  developer-facing pre-mount glitch path, not user copy.

Accessibility
- IconCell drops `aria-label` when `inlineLabel` renders visible
  text — visible labels already provide the accessible name; double
  labelling is the wrong default.
- PreviewCard exposes `<section>` as a named landmark via
  `aria-labelledby` pointing at the title span (uses Vue 3.5's
  `useId()` for a stable id). Screen-reader landmark navigation now
  finds the card.

Layout
- PanelDragPreview no longer applies `style.width` on dock presets
  — the dock width comes from the `w-(--panel-dock-width,440px)`
  Tailwind utility. Mirrors the existing height guard so the ghost
  preview shows the dock width when snapping left/right, not the
  pre-snap floating width.
@eliheuer
Copy link
Copy Markdown
Author

eliheuer commented May 7, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/stores/appModeStore.ts (1)

220-231: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset panel layout fields when a workflow has no saved layout.

loadSelections() only applies fields that exist in data.layout. If the previous workflow had custom panel state and the next one has no layout payload (or only a partial one), panelPreset, panelCollapsed, panelWidthCells, and panelRows keep their old values and bleed into the new workflow.

💡 Suggested fix
 function loadSelections(data: Partial<LinearData> | undefined) {
   const { inputs, outputs } = pruneLinearData(data)
   selectedInputs.value = inputs
   selectedOutputs.value = outputs

   const layout = data?.layout
-  if (!layout) return
-  if (layout.panelPreset) panelPreset.value = layout.panelPreset
-  if (typeof layout.panelCollapsed === 'boolean')
-    panelCollapsed.value = layout.panelCollapsed
-  if (layout.panelWidthCells) panelWidthCells.value = layout.panelWidthCells
-  if (layout.panelRows) panelRows.value = layout.panelRows
+  panelPreset.value = layout?.panelPreset ?? 'right-dock'
+  panelCollapsed.value = layout?.panelCollapsed ?? false
+  panelWidthCells.value = layout?.panelWidthCells ?? 8
+  panelRows.value = layout?.panelRows ?? []
 }
🤖 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 `@src/stores/appModeStore.ts` around lines 220 - 231, The loadSelections
function currently only sets panel state when data.layout fields exist, causing
previous workflow values to persist when the new data has no layout or a partial
layout; update loadSelections to explicitly reset panelPreset, panelCollapsed,
panelWidthCells, and panelRows to their default/empty values when data?.layout
is undefined or when each specific layout field is missing (e.g., if !layout or
typeof layout.panelCollapsed !== 'boolean' then set panelCollapsed.value to its
default); reference the loadSelections function and the reactive refs
panelPreset, panelCollapsed, panelWidthCells, and panelRows to implement these
defensive assignments so state does not bleed between workflows.
src/renderer/extensions/linearMode/LinearPreview.vue (1)

115-131: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate rerun on a successful workflow load.

loadWorkflow() can return early when the asset has no embedded workflow, but rerun() still calls runButtonClick(e) afterward. That queues the current graph instead of the workflow attached to the selected output.

💡 Suggested fix
-async function loadWorkflow(item: AssetItem | undefined) {
-  if (!item) return
+async function loadWorkflow(item: AssetItem | undefined): Promise<boolean> {
+  if (!item) return false
   const { workflow } = await extractWorkflowFromAsset(item)
-  if (!workflow) return
+  if (!workflow) return false

-  if (workflow.id !== app.rootGraph.id) return app.loadGraphData(workflow)
+  if (workflow.id !== app.rootGraph.id) {
+    await app.loadGraphData(workflow)
+    return true
+  }
   const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker
-  if (!changeTracker) return app.loadGraphData(workflow)
+  if (!changeTracker) {
+    await app.loadGraphData(workflow)
+    return true
+  }
   changeTracker.redoQueue = []
   await changeTracker.updateState([workflow], changeTracker.undoQueue)
+  return true
 }

 async function rerun(e: Event) {
   if (!runButtonClick) return
-  await loadWorkflow(selectedItem.value)
+  const loaded = await loadWorkflow(selectedItem.value)
+  if (!loaded) return
   runButtonClick(e)
 }

Based on learnings: For Vue components that handle “rerun” actions from an output, ensure the originating workflow is loaded before running the prompt queue action.

🤖 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 `@src/renderer/extensions/linearMode/LinearPreview.vue` around lines 115 - 131,
loadWorkflow can return early (no embedded workflow) but rerun always calls
runButtonClick; change loadWorkflow to return a boolean success flag (true when
the intended workflow was actually loaded/processed, false when it returned
early) and update its control flow to return true at the end of the successful
path and false on early exits (e.g., when !item or !workflow or when it
delegates to app.loadGraphData without completing changeTracker updates). Then
modify rerun to await const loaded = await loadWorkflow(selectedItem.value) and
only call runButtonClick(e) if loaded === true; reference functions/variables:
loadWorkflow, rerun, runButtonClick, selectedItem, extractWorkflowFromAsset,
useWorkflowStore, app.loadGraphData, changeTracker.updateState.
♻️ Duplicate comments (3)
src/components/appMode/layout/PanelHeader.vue (1)

16-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guarantee accessible labels for the collapsible icon button at the type level.

expandLabel/collapseLabel are optional, but the control at Line 61 is icon-only. With collapsible defaulting to true, callers can omit labels and ship an unnamed button.

♻️ Proposed fix
 const {
   title,
   draggable = false,
   dragging = false,
   collapsible = true,
   menuEntries,
   expandLabel,
   collapseLabel,
   menuLabel
 } = defineProps<{
   title?: string
   draggable?: boolean
   dragging?: boolean
   collapsible?: boolean
   menuEntries: MenuItem[]
-  /** Required when `collapsible` is true so icon-only buttons have accessible names. */
-  expandLabel?: string
-  collapseLabel?: string
+  expandLabel: string
+  collapseLabel: string
   menuLabel: string
 }>()
     <PanelHeader
       v-if="showHeader"
       :title="title || t('linearMode.outputs.title')"
       :draggable="!maximized && !noZoomMode"
       :dragging="dragging"
       :collapsible="false"
+      :expand-label="t('linearMode.floatingPanel.expand')"
+      :collapse-label="t('linearMode.floatingPanel.collapse')"
       :menu-entries="menuEntries"
       :menu-label="t('linearMode.floatingPanel.menu')"
       `@pointerdown`="handleHeaderPointerDown"
     >

Based on learnings: “If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.”

Also applies to: 61-61

🤖 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 `@src/components/appMode/layout/PanelHeader.vue` around lines 16 - 25, The
props currently allow omitting expandLabel/collapseLabel while collapsible
defaults to true, which can produce an unlabeled icon-only button; change the
defineProps declaration so expandLabel and collapseLabel are required (remove
the ? from expandLabel and collapseLabel) to guarantee accessible aria-labels
for the collapsible control, and update any call sites that construct
PanelHeader to supply these labels; reference the props in defineProps and the
collapsible control that reads expandLabel/collapseLabel when rendering the icon
button.
src/assets/css/style.css (1)

113-128: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore explicit keyboard-only focus indicator for themed inputs/pills.

These rules currently suppress rings/outlines but do not provide a :focus-visible replacement, so keyboard focus can become hard to track in App Mode.

♻️ Suggested fix
 .app-mode-themed textarea:focus,
 .app-mode-themed input:focus,
 .app-mode-themed .bg-component-node-widget-background:focus-within {
   border-color: var(--fg-color);
   outline: none;
   box-shadow: none;
 }
 
+.app-mode-themed textarea:focus-visible,
+.app-mode-themed input:focus-visible,
+.app-mode-themed .bg-component-node-widget-background:has(:focus-visible) {
+  outline: 2px solid rgb(255 255 255 / 0.7);
+  outline-offset: 1px;
+}
+
 /* Kill NodeWidgets' focus-within ring; the pill border is the focus signal. */
 .app-mode-themed [class*='focus-within']:focus-within {
   box-shadow: none !important;
   --tw-ring-shadow: 0 0 transparent;
   --tw-ring-offset-shadow: 0 0 transparent;
 }
Based on learnings: “App Mode CSS guideline: In .floating-panel and .app-chrome, mouse focus intentionally uses a subtle border-color change, while keyboard focus must show a visible 2px outline via :focus-visible (also applied to the pill wrapper with :has(:focus-visible)).”
🤖 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 `@src/assets/css/style.css` around lines 113 - 128, The current CSS removes
outlines and focus rings for themed inputs and node widget pills without
providing a keyboard-only focus indicator; restore a visible 2px keyboard focus
outline via :focus-visible for inputs and textareas and add a rule for the pill
wrapper using the :has(:focus-visible) pattern so the pill shows the same 2px
outline when an inner control has keyboard focus. Concretely: keep the subtle
border-color change for mouse focus (use :focus:not(:focus-visible) on
.app-mode-themed textarea/input and pill wrapper) and add :focus-visible rules
on .app-mode-themed textarea:focus-visible, .app-mode-themed input:focus-visible
and .app-mode-themed .bg-component-node-widget-background:has(:focus-visible)
(and include the .floating-panel and .app-chrome variants) to apply a 2px
visible outline; remove blanket outline:none/box-shadow:none only for
:focus-visible so keyboard users see the outline.
src/components/appMode/layout/LayoutView.vue (1)

175-188: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not enter drag mode when pointer capture fails.

Line 181-187 catches capture failures but still sets dragging = true and continues to panBy(...). That can leave drag state active without a valid pointer capture. Reset and return on failure; only flip dragging after successful capture.

🛠️ Proposed fix
 useEventListener(window, 'pointermove', (e: PointerEvent) => {
   if (!dragStart) return
+  const dragButtonDown = (e.buttons & 1) !== 0 || (e.buttons & 4) !== 0
+  if (!dragButtonDown) {
+    endDrag()
+    return
+  }
   if (!dragging) {
     const dx = e.clientX - dragStart.x
     const dy = e.clientY - dragStart.y
     if (dx * dx + dy * dy < DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) return
     try {
       bgRef.value?.setPointerCapture(dragStart.pointerId)
     } catch {
-      // Some browsers reject capture on non-primary pointers.
+      endDrag()
+      return
     }
     dragging = true
   }
   appModeStore.panBy(e.movementX, e.movementY)
 })
🤖 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 `@src/components/appMode/layout/LayoutView.vue` around lines 175 - 188, The
pointermove handler currently sets dragging = true even if
bgRef.value?.setPointerCapture(dragStart.pointerId) throws; change the logic in
the useEventListener callback so that you only set dragging = true after
setPointerCapture succeeds: when detecting movement beyond DRAG_THRESHOLD_PX
using dragStart, attempt setPointerCapture on bgRef and if it throws, reset
dragStart/any temporary state and return early (do not call appModeStore.panBy
or set dragging); on successful capture proceed to set dragging = true and then
call appModeStore.panBy(e.movementX, e.movementY).
🧹 Nitpick comments (14)
src/components/appMode/layout/panels/usePointerDrag.ts (1)

40-41: 💤 Low value

Prefer a function declaration for isOurs.

Pure helper functions in this repo conventionally use function declarations rather than arrow expressions. Same applies to most consumers of this composable.

Suggested change
-  const isOurs = (e: PointerEvent) =>
-    activePointerId !== null && e.pointerId === activePointerId
+  function isOurs(e: PointerEvent): boolean {
+    return activePointerId !== null && e.pointerId === activePointerId
+  }

Based on learnings: "Prefer pure function declarations over function expressions ... for pure functions in the repository."

🤖 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 `@src/components/appMode/layout/panels/usePointerDrag.ts` around lines 40 - 41,
The helper isOurs is currently an arrow function; convert it to a named function
declaration to follow repo conventions for pure helper functions: replace the
const isOurs = (e: PointerEvent) => ... with function isOurs(e: PointerEvent) {
return activePointerId !== null && e.pointerId === activePointerId; } and keep
references to activePointerId and all existing callers unchanged (ensure
export/local scope matches the original).
src/renderer/extensions/linearMode/LinearArrange.vue (2)

89-102: 💤 Low value

cn() with only static class strings is unnecessary.

The :class binding here only composes static strings — no conditional logic — so cn(...) adds runtime work without benefit. Inline the joined string (or move to a class="..." static binding). cn is best reserved for conditional/merge cases.

Suggested change
-      :class="
-        cn(
-          'border bg-primary-background text-(--primary-foreground)',
-          'border-primary-background-hover',
-          'hover:bg-primary-background-hover'
-        )
-      "
+      class="border bg-primary-background text-(--primary-foreground) border-primary-background-hover hover:bg-primary-background-hover"
🤖 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 `@src/renderer/extensions/linearMode/LinearArrange.vue` around lines 89 - 102,
The :class on the Button in LinearArrange.vue is calling cn() with only static
strings; remove the unnecessary runtime helper and replace the bound class with
a static class attribute (e.g., change the :class="cn(...)" expression to a
plain class="..." containing the same combined classes) on the Button that
triggers setMode('builder:outputs') and uses
t('linearMode.arrange.switchToOutputsButton'); ensure the final static class
string preserves the exact class names currently passed to cn.

81-83: 💤 Low value

Using text-warning-background as text color is unusual.

text-warning-background paints the text in the warning background token color, which typically results in a much higher-saturation color than intended for inline text emphasis. If the goal is a warning highlight readable on the parent surface, prefer text-(--warning-foreground) (or whatever semantic token your design system uses for warning text on neutral backgrounds).

🤖 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 `@src/renderer/extensions/linearMode/LinearArrange.vue` around lines 81 - 83,
The span rendering the i18n key t('linearMode.arrange.atLeastOne') uses the
utility class text-warning-background which applies the warning background token
to text; change that class to the semantic warning foreground token (e.g.,
text-warning-foreground or text-[var(--warning-foreground)] depending on your
design tokens) so the inline warning uses the intended foreground color; update
the class on the span in LinearArrange.vue accordingly.
src/components/appMode/layout/panels/usePanelResize.ts (1)

33-50: 💤 Low value

Consider also gating non-mouse pointers on e.isPrimary.

Line 34's e.button !== 0 && e.pointerType === 'mouse' correctly bails on non-primary mouse buttons but lets through any touch/pen pointer. If a multi-finger touch arrives during a panel resize handle press, you may get an unintended secondary pointer driving the resize. Adding && e.isPrimary (or letting the underlying usePointerDrag re-entrance guard fully handle it) is a quick hardening.

🤖 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 `@src/components/appMode/layout/panels/usePanelResize.ts` around lines 33 - 50,
The pointer-start guard in the onStart handler of the usePointerDrag call allows
non-primary touch/pen pointers to start a resize; update the condition in
onStart (the handler where startX, startCells, gridStep are set) to also require
e.isPrimary for non-mouse pointers (or delegate to the existing re-entrance
guard in usePointerDrag) so secondary touch/pen pointers cannot drive changes to
opts.widthCells; ensure the same primary-pointer check is applied consistently
to prevent multi-finger touches from triggering the resize.
src/renderer/extensions/linearMode/outputWindowStore.test.ts (1)

18-18: 💤 Low value

Consider importing MAX_TILES from the store instead of duplicating.

Hardcoded MAX_TILES = 9 will silently disagree with the store if the limit ever changes. Re-exporting it from outputWindowStore.ts (or exposing as a test-internal const) keeps test and impl in sync.

🤖 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 `@src/renderer/extensions/linearMode/outputWindowStore.test.ts` at line 18,
Test hardcodes MAX_TILES = 9 which can drift from the implementation; update the
test to import/re-use the canonical MAX_TILES exported by the store (e.g.,
export MAX_TILES from outputWindowStore.ts and replace the local const in
outputWindowStore.test.ts with an import), or alternatively expose a test-only
constant from the store and consume that in the test so both code and test
reference the same MAX_TILES symbol.
src/components/appMode/layout/panels/PanelDragPreview.vue (1)

13-17: 💤 Low value

Use Vue 3.5 destructured props syntax.

Per repo coding guidelines, prefer destructured props over const props = defineProps<...>(). Then template/script can reference preset/panelHeight/panelWidth directly without props. prefix, and the computeds simplify.

Suggested refactor
-const props = defineProps<{
-  preset: PanelPreset
-  panelHeight?: number
-  panelWidth?: number
-}>()
-
-const presetClass = computed(() => PANEL_PRESET_CLASSES[props.preset])
-const isDocked = computed(() => isDockPreset(props.preset))
-// Skip explicit width + height on docks — docks are slot-sized, so
-// applying the floating-panel dimensions would override the dock's
-// CSS variable widths and bottom anchor.
-const sizeStyle = computed(() => {
-  const style: Record<string, string> = {}
-  if (!isDocked.value && props.panelHeight != null)
-    style.height = `${props.panelHeight}px`
-  if (!isDocked.value && props.panelWidth != null)
-    style.width = `${props.panelWidth}px`
-  return Object.keys(style).length > 0 ? style : undefined
-})
+const { preset, panelHeight, panelWidth } = defineProps<{
+  preset: PanelPreset
+  panelHeight?: number
+  panelWidth?: number
+}>()
+
+const presetClass = computed(() => PANEL_PRESET_CLASSES[preset])
+const isDocked = computed(() => isDockPreset(preset))
+const sizeStyle = computed(() => {
+  const style: Record<string, string> = {}
+  if (!isDocked.value && panelHeight != null)
+    style.height = `${panelHeight}px`
+  if (!isDocked.value && panelWidth != null)
+    style.width = `${panelWidth}px`
+  return Object.keys(style).length > 0 ? style : undefined
+})

As per coding guidelines: "Prefer reactive props destructuring to const props = defineProps<...> in Vue 3.5+ style".

🤖 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 `@src/components/appMode/layout/panels/PanelDragPreview.vue` around lines 13 -
17, The component currently uses const props = defineProps<...>() — switch to
Vue 3.5+ destructured reactive props so template and script can reference
preset, panelHeight, and panelWidth directly; replace defineProps usage with the
destructured form (e.g., const { preset, panelHeight, panelWidth } =
defineProps<{ preset: PanelPreset; panelHeight?: number; panelWidth?: number
}>() or the reactive variant per repo style) and update any computed or
references that use props.* to use the destructured identifiers (preset,
panelHeight, panelWidth) instead.
src/components/builder/BuilderBackdrop.vue (1)

30-74: 🏗️ Heavy lift

Reuse usePointerDrag instead of reimplementing the drag scaffolding.

This file duplicates almost the entire pointer-drag lifecycle (re-entrance, threshold, capture, pointercancel, blur reset) that was just centralized in usePointerDrag.ts. Switching to the shared composable removes ~30 lines, automatically picks up the re-entrance guard, and keeps lifecycle behavior consistent across all App Mode pointer interactions.

Sketch
const { start: startPan } = usePointerDrag({
  threshold: DRAG_THRESHOLD_PX,
  onStart: (e) => {
    if (e.button !== 0 && e.button !== 1) return false
  },
  onMove: (e) => appModeStore.panBy(e.movementX, e.movementY)
})
// template: `@pointerdown`="startPan"
🤖 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 `@src/components/builder/BuilderBackdrop.vue` around lines 30 - 74, Replace the
custom pointer-drag scaffolding (DRAG_THRESHOLD_PX, dragStart, dragging,
handlePointerDown, endDrag, the useEventListener calls and the focused watcher)
with the shared composable usePointerDrag: import and call usePointerDrag({
threshold: DRAG_THRESHOLD_PX, onStart: (e) => { if (e.button !== 0 && e.button
!== 1) return false }, onMove: (e) => appModeStore.panBy(e.movementX,
e.movementY) }), remove manual pointer capture/release logic that references
bgRef and dragStart, and wire the template pointerdown to the returned start
function (e.g. `@pointerdown`="startPan"); this keeps the re-entrance guard,
threshold, capture, cancel and blur behavior centralized and eliminates the
duplicated lifecycle code.
src/components/builder/AppBuilder.vue (1)

105-110: 💤 Low value

The void viewport.value reactivity hack is unnecessary.

The inner toViewport() calls invoked from widgetBounds / nodeBounds already read viewport.value, so dependency tracking is established without the explicit void viewport.value. Recommend removing the dead access (and the explanatory comment) since the reactivity already follows from the bounds computation paths used inside this computed.

♻️ Proposed cleanup
   const g = canvas.graph
   if (!g) return []
-  // Read viewport so the computed re-runs as canvas state updates.
-  void viewport.value
-
   if (isSelectInputsMode.value) {
🤖 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 `@src/components/builder/AppBuilder.vue` around lines 105 - 110, Remove the
unnecessary reactivity hack in the candidates computed: delete the explicit
"void viewport.value" access and its comment inside the candidates computed
function since widgetBounds / nodeBounds already read viewport.value and
establish the dependency; keep the rest of the logic that references
active.value, canvas.graph (g), and the bounds-based paths
(widgetBounds/nodeBounds) so dependency tracking remains intact.
src/components/appMode/layout/AppChrome.vue (3)

194-197: 💤 Low value

cellTitle shows runDisabledHint for any disabled cell, including system-batch-count.

DISABLE_IN_BUILDER includes both system-batch-count and system-run, but cellTitle() returns the same linearMode.builder.runDisabledHint for either. The batch-count cell will tooltip with a "run is disabled" hint that doesn't describe what the cell actually represents. Consider either branching on cell.kind or using a more generic hint key.

♻️ Proposed adjustment
 function cellTitle(cell: ChromeCell): string | undefined {
-  if (cell.disabled) return t('linearMode.builder.runDisabledHint')
+  if (!cell.disabled) return undefined
+  if (cell.kind === 'system-run') return t('linearMode.builder.runDisabledHint')
+  if (cell.kind === 'system-batch-count') {
+    return t('linearMode.builder.batchCountDisabledHint')
+  }
   return undefined
 }
🤖 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 `@src/components/appMode/layout/AppChrome.vue` around lines 194 - 197,
cellTitle currently returns the same run-disabled message for any ChromeCell
with cell.disabled (function cellTitle), which causes the wrong tooltip for
DISABLE_IN_BUILDER items like "system-batch-count"; update cellTitle to branch
on cell.kind (or inspect ChromeCell identifiers such as "system-batch-count" vs
"system-run") and return a more specific translation key for batch-count (or a
generic disabled hint key when kind is unknown), e.g., map cell.kind to distinct
t(...) keys or fall back to a common t('linearMode.builder.disabledHint') so the
batch-count cell shows an appropriate tooltip instead of the run-disabled text.

92-103: 💤 Low value

Prefer function declarations over arrow expressions for these handlers.

navZoomIn, navZoomOut, and navResetView are pure handlers without closure-specific semantics; per the repo's coding guidelines, prefer function declarations.

♻️ Proposed refactor
-const navZoomIn = () => {
-  if (useAppModeZoom.value) return zoomIn()
-  dispatchCanvas('Comfy.Canvas.ZoomIn')
-}
-const navZoomOut = () => {
-  if (useAppModeZoom.value) return zoomOut()
-  dispatchCanvas('Comfy.Canvas.ZoomOut')
-}
-const navResetView = () => {
-  if (useAppModeZoom.value) return resetView()
-  dispatchCanvas('Comfy.Canvas.FitView')
-}
+function navZoomIn() {
+  if (useAppModeZoom.value) return zoomIn()
+  dispatchCanvas('Comfy.Canvas.ZoomIn')
+}
+function navZoomOut() {
+  if (useAppModeZoom.value) return zoomOut()
+  dispatchCanvas('Comfy.Canvas.ZoomOut')
+}
+function navResetView() {
+  if (useAppModeZoom.value) return resetView()
+  dispatchCanvas('Comfy.Canvas.FitView')
+}

As per coding guidelines: "Do not use function expressions if it's possible to use function declarations instead."

🤖 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 `@src/components/appMode/layout/AppChrome.vue` around lines 92 - 103, The
handlers navZoomIn, navZoomOut and navResetView are defined as arrow function
expressions; replace them with function declarations to follow the repo
guideline (avoid function expressions when a declaration is possible). Change
the three const arrow definitions for navZoomIn/navZoomOut/navResetView to named
function declarations that check useAppModeZoom.value and call
zoomIn/zoomOut/resetView or otherwise call
dispatchCanvas('Comfy.Canvas.ZoomIn'|'Comfy.Canvas.ZoomOut'|'Comfy.Canvas.FitView')
preserving the exact logic and references to useAppModeZoom, zoomIn, zoomOut,
resetView and dispatchCanvas.

71-74: 💤 Low value

Conditional lifecycle hook registration is brittle if variant ever changes.

Registering onMounted/onBeforeUnmount inside an if based on the destructured prop value works only because the parent re-mounts the component when switching modes. With Vue 3.5 reactive props destructuring, variant becomes reactive elsewhere, which can give the false impression that flipping variant later will re-evaluate this branch — it won't (lifecycle hooks register only once at setup). Consider documenting the assumption, or using watch(() => variant, ...) with onScopeDispose for explicit lifecycle handling.

🤖 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 `@src/components/appMode/layout/AppChrome.vue` around lines 71 - 74, The
current conditional registration of onMounted/onBeforeUnmount based on the
destructured prop variant is brittle because lifecycle hooks run only once at
setup; change to explicitly handle reactive changes by either (A) always
registering the lifecycle hooks and inside onMounted call
canvasStore.initScaleSync() only if variant === 'builder' and call
canvasStore.cleanupScaleSync() inside onBeforeUnmount accordingly, or (B) use
watch(() => variant, (v, old) => { if (v === 'builder')
canvasStore.initScaleSync(); if (old === 'builder' && v !== 'builder')
canvasStore.cleanupScaleSync(); }) and ensure cleanup via onScopeDispose or a
paired cleanup call to canvasStore.cleanupScaleSync(); update comments to
document the assumption if you keep the original approach.
src/components/appMode/layout/panels/usePanelDrag.ts (1)

57-71: 💤 Low value

Recompute anchors once per drag, not per pointermove.

nearestPreset calls presetAnchors, which calls getComputedStyle(document.documentElement) twice per invocation. These run on every pointermove while dragging. Not a hot-path emergency, but caching the anchors at onStart (and refreshing on resize) avoids repeated layout reads.

♻️ Possible optimization
 export function usePanelDrag(opts: UsePanelDragOptions) {
   const snapTarget = ref<PanelPreset>(opts.currentPreset.value)
+  let cachedAnchors: PresetAnchor[] | null = null
 
   const { isDragging, start: onHeaderPointerDown } = usePointerDrag({
     threshold: DRAG_THRESHOLD_PX,
     stopPropagation: true,
-    onStart: (e) => !(e.button !== 0 && e.pointerType === 'mouse'),
+    onStart: (e) => {
+      if (e.button !== 0 && e.pointerType === 'mouse') return false
+      cachedAnchors = presetAnchors(window.innerWidth, window.innerHeight)
+      return true
+    },
     onMove: (e) => {
-      snapTarget.value = nearestPreset(e.clientX, e.clientY)
+      snapTarget.value = nearestPreset(cachedAnchors!, e.clientX, e.clientY)
     },
     onCommit: () => {
+      cachedAnchors = null
       if (snapTarget.value !== opts.currentPreset.value) {
         opts.onCommit(snapTarget.value)
       }
     }
   })

(also adjust nearestPreset to accept the cached anchors)

🤖 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 `@src/components/appMode/layout/panels/usePanelDrag.ts` around lines 57 - 71,
nearestPreset currently calls presetAnchors on every pointermove (causing
repeated getComputedStyle reads); instead compute and cache anchors once at drag
start and refresh on window resize, then pass the cached anchors into
nearestPreset (change nearestPreset signature to accept anchors: PanelAnchor[]
or similar) so pointermove handlers use the cached anchors; update the onStart
handler to call presetAnchors(window.innerWidth, window.innerHeight) and store
the result, add a resize listener to refresh that cache, and remove any direct
calls to presetAnchors from the pointermove path.
src/components/appMode/layout/OutputWindowList.vue (1)

78-95: 💤 Low value

Aspect-probe watcher leaks Image objects on rapid URL changes.

Each watcher trigger creates a fresh new Image() per entry without releasing the previous probe. If a window's URL flips multiple times before the first onload fires (latent → final, or replays), several probes accumulate, all writing back via attachAspect. The race-guard on Line 87 prevents stale writes, but doesn't free the in-flight Image requests. Consider tracking the active probe per id and aborting/clearing previous ones when a new URL arrives, or use URL-keyed dedupe.

♻️ Suggested approach
+const probes = new Map<string, HTMLImageElement>()
 watch(
   () => sortedWindows.value.map((w) => ({ id: w.id, url: aspectSourceUrl(w) })),
   (entries) => {
     for (const { id, url } of entries) {
-      if (!url) continue
-      const probe = new Image()
+      if (!url) {
+        probes.delete(id)
+        continue
+      }
+      const existing = probes.get(id)
+      if (existing && existing.src === url) continue
+      const probe = new Image()
+      probes.set(id, probe)
       probe.onload = () => {
+        if (probes.get(id) !== probe) return
         const current = windowStore.windows.find((w) => w.id === id)
         if (!current || aspectSourceUrl(current) !== url) return
         if (probe.naturalWidth <= 0 || probe.naturalHeight <= 0) return
         windowStore.attachAspect(id, probe.naturalWidth / probe.naturalHeight)
       }
       probe.src = url
     }
   },
   { immediate: true, deep: true }
 )
🤖 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 `@src/components/appMode/layout/OutputWindowList.vue` around lines 78 - 95, The
watcher creates a new Image probe for each entry and never cancels prior probes,
leaking requests; fix by tracking active probes in a Map keyed by window id (or
URL) inside the component, and when a new probe is started for the same id:
detach handlers and cancel the previous probe (e.g. prev.onload = prev.onerror =
null; prev.src = ''; delete map entry) before creating the new Image, then set
the new probe in the map and in its onload verify aspectSourceUrl/current id as
now, call windowStore.attachAspect(id, ...) and remove the probe from the map;
also ensure the map is cleared on component unmount to fully release in-flight
probes.
src/components/appMode/layout/panels/useBlockDrag.ts (1)

23-27: 💤 Low value

Optional: consider documenting or guarding overlap when rect.width < 2 * edgeZone.

When a block is narrower than 2 * EDGE_MIN_PX (~80px), leftEdge can exceed rightEdge, so the entire block becomes a columnBefore zone (because that branch is checked first) and columnAfter is unreachable for that block. Probably acceptable in practice given typical grid cell sizes (8–19 cells), but worth a brief note or a clamp if you ever expect very narrow blocks.

🤖 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 `@src/components/appMode/layout/panels/useBlockDrag.ts` around lines 23 - 27,
The left/right edge zones can overlap when rect.width < 2 * edgeZone (computed
from EDGE_FRACTION clamped by EDGE_MIN_PX/EDGE_MAX_PX), making columnAfter
unreachable; in useBlockDrag compute edgeZonePx = clamp(rect.width *
EDGE_FRACTION, EDGE_MIN_PX, EDGE_MAX_PX) then ensure leftEdge = rect.x +
edgeZonePx and rightEdge = rect.x + rect.width - edgeZonePx but if leftEdge >
rightEdge resolve by clamping rightEdge = Math.max(rightEdge, leftEdge) or by
setting both edges to rect.x + rect.width/2 so zones don't overlap; update any
logic that uses leftEdge/rightEdge (and HYSTERESIS_PX, DRAG_THRESHOLD_PX)
accordingly.
🤖 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 `@src/components/appMode/layout/cells/InputCell.vue`:
- Around line 95-101: Update the Tailwind responsive variant in InputCell.vue:
in the NodeWidgets class string (the attribute on the NodeWidgets component)
replace the invalid `not-md:` prefix with the correct breakpoint variant
`max-md:` so the token `not-md:**:[.h-7]:h-10` becomes `max-md:**:[.h-7]:h-10`;
ensure this change is applied to the class on the NodeWidgets element so
Tailwind v4 generates the intended styles.

In `@src/components/appMode/layout/cells/JobQueueCell.vue`:
- Around line 16-22: The handleClick function currently swallows errors because
it calls commandStore.execute('Comfy.Queue.ToggleOverlay') with void; change it
to surface failures using the existing toastErrorHandler pattern (e.g., await
the promise and pass any thrown error to toastErrorHandler or wrap the call in a
.catch that calls toastErrorHandler) so users see a friendly actionable error
when commandStore.execute in handleClick fails; reference the handleClick
function and the commandStore.execute('Comfy.Queue.ToggleOverlay') invocation
and use toastErrorHandler as used elsewhere in the PR.

In `@src/components/appMode/layout/OutputWindowList.vue`:
- Line 274: Replace the background utility alias in OutputWindowList.vue with
the arbitrary CSS-variable form to match the foreground pattern: change
occurrences of bg-success-background (and any bg-destructive-background usages
found in this component or paired RunCell.vue) to the arbitrary variable syntax
bg-[(--success-background)] (or bg-[(--destructive-background)]) so the
component uses the same CSS-variable approach as the existing
text-(--success-foreground) and text-(--destructive-foreground) usages; update
any class strings that include these aliases in the template (e.g., the element
using class="h-full bg-success-background ...") to the new arbitrary-variable
form.

In `@src/components/appMode/layout/panels/PanelBlockList.vue`:
- Around line 143-147: The CSS rule targeting
.panel-block__input[data-multiline='true'] :deep(textarea) relies on
field-sizing: content which Firefox doesn't support, so add a JS fallback in the
PanelBlockList.vue component: implement a mounted() hook that detects Firefox
(or feature-tests for field-sizing support) and, for matching textareas,
attaches an autosize routine (e.g., initAutosize / adjustTextareaHeight) that
sets height = 'auto' then height = scrollHeight on input and window resize, and
remove listeners in beforeUnmount() to avoid leaks; alternatively integrate a
small autosize helper or library and initialize it only when the CSS feature is
unsupported.

In `@src/components/appMode/layout/panels/panelPresetClasses.ts`:
- Around line 11-18: The Tailwind arbitrary calc() values in TOP_BELOW_CHROME,
BOTTOM_ABOVE_CHROME and SLOT_MAX_H are missing required whitespace around + and
-; update those string constants to use underscores (_) as space placeholders
inside the calc expressions (e.g. use _+_ and _-_ between terms) so Tailwind
emits valid CSS, and also fix RIGHT_EDGE and LEFT_EDGE to use the correct
arbitrary var syntax (use a bracketed arbitrary value for
var(--spacing-layout-outer) instead of the current parenthesis form); modify the
constants TOP_BELOW_CHROME, BOTTOM_ABOVE_CHROME, SLOT_MAX_H, RIGHT_EDGE and
LEFT_EDGE accordingly.

In `@src/components/builder/AppBuilder.vue`:
- Around line 131-135: The code uses widget.name (IBaseWidget.name) without
guarding for undefined, which can produce keys like "<id>:undefined" and
collide; update the AppBuilder.vue rendering logic to skip widgets with no
stable name (widget.name === undefined) before constructing storeName/storeId,
SelectionChrome's key (`\`${node.id}:${widget.name}\``), and before calling
appModeStore.toggleSelectedInput; specifically filter out or return early for
widgets where widget.name is undefined so appModeStore.selectedInputs,
isSelected, onToggle, and the key only handle widgets with a defined name
(instead of relying on String(widget.name) or generating "undefined" keys).

In `@src/components/builder/BuilderBackdrop.vue`:
- Around line 34-38: handlePointerDown can overwrite an active drag session
because it doesn't guard against re-entrance; add an early return when a drag
session is already active by checking the existing dragStart before setting a
new one to avoid leaking pointer capture and desyncing the session (match the
behavior of usePointerDrag.start()); update the handlePointerDown function to
bail if dragStart is non-null/defined so only the first pointerdown starts the
drag.

In `@src/components/builder/BuilderPanel.vue`:
- Around line 39-41: The fixed panel currently hardcodes reserved space with
left-(--sidebar-width,0px) which only accounts for a left sidebar; change the
positioning to use logical inset properties so the panel respects sidebar
location (e.g., replace the left-(--sidebar-width,0px) usage in the root div of
BuilderPanel.vue with an inline logical inset such as inset-inline-start using
the same --sidebar-width fallback or compute the inset from the
Comfy.Sidebar.Location and apply a conditional class/inline style); update the
class/attribute on the div (the element with class "pointer-events-none fixed
...") or the component's computed style so it uses inset-inline-start:
var(--sidebar-width, 0px) (or conditionally uses inset-inline-end when location
=== 'right') instead of a hardcoded left value.

In `@src/components/builder/BuilderToolbar.vue`:
- Around line 59-77: The step title/subtitle strings (selectInputsStep,
selectOutputsStep, arrangeStep) call t() once at setup and become static so they
won't update on locale change; instead wrap the steps array in a computed() that
returns the three step objects (calling t() inside the computed) so
titles/subtitles recompute when i18n.global.locale.value changes—update the
definition of steps to be a computed property that builds objects using t()
rather than storing the t() results at setup time.

In `@src/components/graph/GraphCanvas.vue`:
- Around line 21-23: The sidebar is blocked in builder mode because
sidebarPanelVisible is computed as activeSidebarTab.value !== null &&
!isBuilderMode.value; update LiteGraphCanvasSplitterOverlay.vue to allow opening
the sidebar in builder mode by removing the "!isBuilderMode.value" check (so
sidebarPanelVisible derives only from activeSidebarTab), or alternatively in
GraphCanvas.vue conditionally omit the <template `#side-toolbar`> (and
SideToolbar) when isBuilderMode is true; adjust the logic around the unique
symbols sidebarPanelVisible, activeSidebarTab, isBuilderMode, SideToolbar and
showUI so toolbar buttons are only shown when the underlying sidebar slot can
actually open.

In `@src/renderer/extensions/linearMode/outputWindowStore.ts`:
- Around line 141-165: The code incorrectly treats a left-docked panel as x ===
0; update the panel-side detection and boundary logic to account for the
chrome-rail offset used by computeAvailRect (compare against CHROME_OUTER).
Concretely, set panelOnLeft using insets.panelRect.x === CHROME_OUTER (instead
of === 0), set featureOnRight from panelOnLeft as before, and update the
rightBoundaryWithPanel/leftBoundaryWithPanel conditions to test panelRect.x >
CHROME_OUTER and panelRect.x === CHROME_OUTER respectively so the left-edge
sliver correction and feature placement behave correctly (references:
insets.panelRect, panelOnLeft, featureOnRight, rightBoundaryWithPanel,
leftBoundaryWithPanel, computeAvailRect).

In `@src/renderer/extensions/linearMode/SelectionChrome.vue`:
- Around line 17-31: The interactive div in SelectionChrome.vue is currently
pointer-only; add button semantics by adding role="button" and tabindex="0" to
the element and wire keyboard handlers so pressing Enter or Space triggers the
same toggle emit used by the pointer handler (emit 'toggle'); implement a
keydown handler (can be inline or a method like onKeyDown) that checks for Enter
(key === 'Enter') or Space (key === ' ' or key === 'Spacebar' or code ===
'Space'), calls $emit('toggle'), and prevents default for Space to avoid page
scrolling; keep existing pointer capture.stop.prevent modifiers and ensure the
keydown handler also uses stop/prevent to match behavior.

In `@src/views/LinearView.vue`:
- Around line 124-145: The Splitter always places the extension panel on the
left; update the render logic to honor the stored sidebar location (e.g.,
Comfy.Sidebar.Location / sidebarOnLeft) so the extension panel appears on the
correct side in the new layout branch: read the same boolean used elsewhere
(sidebarOnLeft) and, inside the template that renders Splitter and its two
SplitterPanel children (the block using activeTab, SIDE_PANEL_SIZE, and
LayoutView), conditionally swap the order of the panels (or set a
Splitter/reverse prop if available) so when sidebarOnLeft is false the
LayoutView panel is rendered first and the ExtensionSlot panel second, and when
sidebarOnLeft is true keep the current order. Ensure you reference the same
state key/state-storage semantics already used for the splitter.

---

Outside diff comments:
In `@src/renderer/extensions/linearMode/LinearPreview.vue`:
- Around line 115-131: loadWorkflow can return early (no embedded workflow) but
rerun always calls runButtonClick; change loadWorkflow to return a boolean
success flag (true when the intended workflow was actually loaded/processed,
false when it returned early) and update its control flow to return true at the
end of the successful path and false on early exits (e.g., when !item or
!workflow or when it delegates to app.loadGraphData without completing
changeTracker updates). Then modify rerun to await const loaded = await
loadWorkflow(selectedItem.value) and only call runButtonClick(e) if loaded ===
true; reference functions/variables: loadWorkflow, rerun, runButtonClick,
selectedItem, extractWorkflowFromAsset, useWorkflowStore, app.loadGraphData,
changeTracker.updateState.

In `@src/stores/appModeStore.ts`:
- Around line 220-231: The loadSelections function currently only sets panel
state when data.layout fields exist, causing previous workflow values to persist
when the new data has no layout or a partial layout; update loadSelections to
explicitly reset panelPreset, panelCollapsed, panelWidthCells, and panelRows to
their default/empty values when data?.layout is undefined or when each specific
layout field is missing (e.g., if !layout or typeof layout.panelCollapsed !==
'boolean' then set panelCollapsed.value to its default); reference the
loadSelections function and the reactive refs panelPreset, panelCollapsed,
panelWidthCells, and panelRows to implement these defensive assignments so state
does not bleed between workflows.

---

Duplicate comments:
In `@src/assets/css/style.css`:
- Around line 113-128: The current CSS removes outlines and focus rings for
themed inputs and node widget pills without providing a keyboard-only focus
indicator; restore a visible 2px keyboard focus outline via :focus-visible for
inputs and textareas and add a rule for the pill wrapper using the
:has(:focus-visible) pattern so the pill shows the same 2px outline when an
inner control has keyboard focus. Concretely: keep the subtle border-color
change for mouse focus (use :focus:not(:focus-visible) on .app-mode-themed
textarea/input and pill wrapper) and add :focus-visible rules on
.app-mode-themed textarea:focus-visible, .app-mode-themed input:focus-visible
and .app-mode-themed .bg-component-node-widget-background:has(:focus-visible)
(and include the .floating-panel and .app-chrome variants) to apply a 2px
visible outline; remove blanket outline:none/box-shadow:none only for
:focus-visible so keyboard users see the outline.

In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 175-188: The pointermove handler currently sets dragging = true
even if bgRef.value?.setPointerCapture(dragStart.pointerId) throws; change the
logic in the useEventListener callback so that you only set dragging = true
after setPointerCapture succeeds: when detecting movement beyond
DRAG_THRESHOLD_PX using dragStart, attempt setPointerCapture on bgRef and if it
throws, reset dragStart/any temporary state and return early (do not call
appModeStore.panBy or set dragging); on successful capture proceed to set
dragging = true and then call appModeStore.panBy(e.movementX, e.movementY).

In `@src/components/appMode/layout/PanelHeader.vue`:
- Around line 16-25: The props currently allow omitting
expandLabel/collapseLabel while collapsible defaults to true, which can produce
an unlabeled icon-only button; change the defineProps declaration so expandLabel
and collapseLabel are required (remove the ? from expandLabel and collapseLabel)
to guarantee accessible aria-labels for the collapsible control, and update any
call sites that construct PanelHeader to supply these labels; reference the
props in defineProps and the collapsible control that reads
expandLabel/collapseLabel when rendering the icon button.

---

Nitpick comments:
In `@src/components/appMode/layout/AppChrome.vue`:
- Around line 194-197: cellTitle currently returns the same run-disabled message
for any ChromeCell with cell.disabled (function cellTitle), which causes the
wrong tooltip for DISABLE_IN_BUILDER items like "system-batch-count"; update
cellTitle to branch on cell.kind (or inspect ChromeCell identifiers such as
"system-batch-count" vs "system-run") and return a more specific translation key
for batch-count (or a generic disabled hint key when kind is unknown), e.g., map
cell.kind to distinct t(...) keys or fall back to a common
t('linearMode.builder.disabledHint') so the batch-count cell shows an
appropriate tooltip instead of the run-disabled text.
- Around line 92-103: The handlers navZoomIn, navZoomOut and navResetView are
defined as arrow function expressions; replace them with function declarations
to follow the repo guideline (avoid function expressions when a declaration is
possible). Change the three const arrow definitions for
navZoomIn/navZoomOut/navResetView to named function declarations that check
useAppModeZoom.value and call zoomIn/zoomOut/resetView or otherwise call
dispatchCanvas('Comfy.Canvas.ZoomIn'|'Comfy.Canvas.ZoomOut'|'Comfy.Canvas.FitView')
preserving the exact logic and references to useAppModeZoom, zoomIn, zoomOut,
resetView and dispatchCanvas.
- Around line 71-74: The current conditional registration of
onMounted/onBeforeUnmount based on the destructured prop variant is brittle
because lifecycle hooks run only once at setup; change to explicitly handle
reactive changes by either (A) always registering the lifecycle hooks and inside
onMounted call canvasStore.initScaleSync() only if variant === 'builder' and
call canvasStore.cleanupScaleSync() inside onBeforeUnmount accordingly, or (B)
use watch(() => variant, (v, old) => { if (v === 'builder')
canvasStore.initScaleSync(); if (old === 'builder' && v !== 'builder')
canvasStore.cleanupScaleSync(); }) and ensure cleanup via onScopeDispose or a
paired cleanup call to canvasStore.cleanupScaleSync(); update comments to
document the assumption if you keep the original approach.

In `@src/components/appMode/layout/OutputWindowList.vue`:
- Around line 78-95: The watcher creates a new Image probe for each entry and
never cancels prior probes, leaking requests; fix by tracking active probes in a
Map keyed by window id (or URL) inside the component, and when a new probe is
started for the same id: detach handlers and cancel the previous probe (e.g.
prev.onload = prev.onerror = null; prev.src = ''; delete map entry) before
creating the new Image, then set the new probe in the map and in its onload
verify aspectSourceUrl/current id as now, call windowStore.attachAspect(id, ...)
and remove the probe from the map; also ensure the map is cleared on component
unmount to fully release in-flight probes.

In `@src/components/appMode/layout/panels/PanelDragPreview.vue`:
- Around line 13-17: The component currently uses const props =
defineProps<...>() — switch to Vue 3.5+ destructured reactive props so template
and script can reference preset, panelHeight, and panelWidth directly; replace
defineProps usage with the destructured form (e.g., const { preset, panelHeight,
panelWidth } = defineProps<{ preset: PanelPreset; panelHeight?: number;
panelWidth?: number }>() or the reactive variant per repo style) and update any
computed or references that use props.* to use the destructured identifiers
(preset, panelHeight, panelWidth) instead.

In `@src/components/appMode/layout/panels/useBlockDrag.ts`:
- Around line 23-27: The left/right edge zones can overlap when rect.width < 2 *
edgeZone (computed from EDGE_FRACTION clamped by EDGE_MIN_PX/EDGE_MAX_PX),
making columnAfter unreachable; in useBlockDrag compute edgeZonePx =
clamp(rect.width * EDGE_FRACTION, EDGE_MIN_PX, EDGE_MAX_PX) then ensure leftEdge
= rect.x + edgeZonePx and rightEdge = rect.x + rect.width - edgeZonePx but if
leftEdge > rightEdge resolve by clamping rightEdge = Math.max(rightEdge,
leftEdge) or by setting both edges to rect.x + rect.width/2 so zones don't
overlap; update any logic that uses leftEdge/rightEdge (and HYSTERESIS_PX,
DRAG_THRESHOLD_PX) accordingly.

In `@src/components/appMode/layout/panels/usePanelDrag.ts`:
- Around line 57-71: nearestPreset currently calls presetAnchors on every
pointermove (causing repeated getComputedStyle reads); instead compute and cache
anchors once at drag start and refresh on window resize, then pass the cached
anchors into nearestPreset (change nearestPreset signature to accept anchors:
PanelAnchor[] or similar) so pointermove handlers use the cached anchors; update
the onStart handler to call presetAnchors(window.innerWidth, window.innerHeight)
and store the result, add a resize listener to refresh that cache, and remove
any direct calls to presetAnchors from the pointermove path.

In `@src/components/appMode/layout/panels/usePanelResize.ts`:
- Around line 33-50: The pointer-start guard in the onStart handler of the
usePointerDrag call allows non-primary touch/pen pointers to start a resize;
update the condition in onStart (the handler where startX, startCells, gridStep
are set) to also require e.isPrimary for non-mouse pointers (or delegate to the
existing re-entrance guard in usePointerDrag) so secondary touch/pen pointers
cannot drive changes to opts.widthCells; ensure the same primary-pointer check
is applied consistently to prevent multi-finger touches from triggering the
resize.

In `@src/components/appMode/layout/panels/usePointerDrag.ts`:
- Around line 40-41: The helper isOurs is currently an arrow function; convert
it to a named function declaration to follow repo conventions for pure helper
functions: replace the const isOurs = (e: PointerEvent) => ... with function
isOurs(e: PointerEvent) { return activePointerId !== null && e.pointerId ===
activePointerId; } and keep references to activePointerId and all existing
callers unchanged (ensure export/local scope matches the original).

In `@src/components/builder/AppBuilder.vue`:
- Around line 105-110: Remove the unnecessary reactivity hack in the candidates
computed: delete the explicit "void viewport.value" access and its comment
inside the candidates computed function since widgetBounds / nodeBounds already
read viewport.value and establish the dependency; keep the rest of the logic
that references active.value, canvas.graph (g), and the bounds-based paths
(widgetBounds/nodeBounds) so dependency tracking remains intact.

In `@src/components/builder/BuilderBackdrop.vue`:
- Around line 30-74: Replace the custom pointer-drag scaffolding
(DRAG_THRESHOLD_PX, dragStart, dragging, handlePointerDown, endDrag, the
useEventListener calls and the focused watcher) with the shared composable
usePointerDrag: import and call usePointerDrag({ threshold: DRAG_THRESHOLD_PX,
onStart: (e) => { if (e.button !== 0 && e.button !== 1) return false }, onMove:
(e) => appModeStore.panBy(e.movementX, e.movementY) }), remove manual pointer
capture/release logic that references bgRef and dragStart, and wire the template
pointerdown to the returned start function (e.g. `@pointerdown`="startPan"); this
keeps the re-entrance guard, threshold, capture, cancel and blur behavior
centralized and eliminates the duplicated lifecycle code.

In `@src/renderer/extensions/linearMode/LinearArrange.vue`:
- Around line 89-102: The :class on the Button in LinearArrange.vue is calling
cn() with only static strings; remove the unnecessary runtime helper and replace
the bound class with a static class attribute (e.g., change the :class="cn(...)"
expression to a plain class="..." containing the same combined classes) on the
Button that triggers setMode('builder:outputs') and uses
t('linearMode.arrange.switchToOutputsButton'); ensure the final static class
string preserves the exact class names currently passed to cn.
- Around line 81-83: The span rendering the i18n key
t('linearMode.arrange.atLeastOne') uses the utility class
text-warning-background which applies the warning background token to text;
change that class to the semantic warning foreground token (e.g.,
text-warning-foreground or text-[var(--warning-foreground)] depending on your
design tokens) so the inline warning uses the intended foreground color; update
the class on the span in LinearArrange.vue accordingly.

In `@src/renderer/extensions/linearMode/outputWindowStore.test.ts`:
- Line 18: Test hardcodes MAX_TILES = 9 which can drift from the implementation;
update the test to import/re-use the canonical MAX_TILES exported by the store
(e.g., export MAX_TILES from outputWindowStore.ts and replace the local const in
outputWindowStore.test.ts with an import), or alternatively expose a test-only
constant from the store and consume that in the test so both code and test
reference the same MAX_TILES symbol.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +95 to +101
:inert="variant === 'builder' || undefined"
>
<DropZone>
<NodeWidgets
:node-data="entry.nodeData"
class="gap-y-3 rounded-lg py-1 [&_textarea]:resize-y **:[.col-span-2]:grid-cols-1 not-md:**:[.h-7]:h-10"
/>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify not-md: variant usage in the codebase (confirm it's used elsewhere and not a one-off)
rg -n "not-md:" --type-list | head -5
rg -rn "not-md:" src/ --include="*.vue" --include="*.ts" -C2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 902


🏁 Script executed:

#!/bin/bash
# Corrected search for not-md: usage
rg "not-md:" src/ -C 2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2631


🌐 Web query:

Tailwind CSS v4 not-md breakpoint modifier responsive variant syntax

💡 Result:

In Tailwind CSS v4, there is no direct "not-md" breakpoint modifier for responsive variants. Responsive variants use min-width media queries by default (mobile-first), with syntax like md:bg-red-500 applying at 48rem (768px) and above [1]. To target below md (not at md and above), use max-md:bg-red-500, which generates @media (width < 48rem) [1]. For a specific md-only range, stack md:max-lg:bg-red-500 [1]. The new not-* variant negates selectors, pseudo-classes, or media/feature queries like not-hover:opacity-75 or not-supports-[display:grid]:flex, but not standard breakpoint prefixes like not-md: [2][3]. No sources show not-md: syntax; not-* works with media variants but breakpoint prefixes follow standard sm:, md:, max-md: patterns [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
# Find Tailwind config files
fd -e "tailwind.config" . -o -e "tailwind.config.ts" . -o -e "tailwind.config.js" .

# Also check for plugin definitions
rg "not-md" . --include="*.ts" --include="*.js" --include="*.config.ts" --include="*.config.js" -B 3 -A 3

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 629


🏁 Script executed:

#!/bin/bash
# Check the actual tailwind config more carefully
find . -name "tailwind.config.*" -type f

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Simpler search for tailwind config
ls -la | grep tailwind
git ls-files | grep -i tailwind.config

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Check package.json and look for tailwind in project structure
cat package.json | grep -A 5 -B 5 tailwind 2>/dev/null || echo "No tailwind in package.json"
git ls-files | head -30

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2844


🏁 Script executed:

#!/bin/bash
# Search all files for tailwind.config
git ls-files | grep -i "tailwind.config"

# Also check for postcss.config
git ls-files | grep -i "postcss.config"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Search in the monorepo structure - check workspace packages
git ls-files | grep -E "(tailwind|tailwind-utils)" | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 238


🏁 Script executed:

#!/bin/bash
# Check the tailwind-utils package which likely defines custom variants
cat packages/tailwind-utils/src/index.ts

# Also search for tailwind.config in all workspaces
git ls-files | grep "\.config\." | grep -v node_modules | grep -v ".vscode"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 841


🏁 Script executed:

#!/bin/bash
# Search the entire codebase for "not-md" definition or plugin registration
rg "not-md" . -t ts -t js -B 5 -A 5 2>/dev/null | head -50

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3992


Replace not-md: with max-md: on line 100 — not-md: is not a valid Tailwind v4 variant.

The class string uses not-md:**:[.h-7]:h-10, but Tailwind v4's official responsive syntax does not include not-md:. The correct variant for "below the md breakpoint" is max-md: (which generates @media (width < 48rem)). The not-* modifier in v4 negates selectors and pseudo-classes, not breakpoint prefixes. Replace with: max-md:**:[.h-7]:h-10.

🤖 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 `@src/components/appMode/layout/cells/InputCell.vue` around lines 95 - 101,
Update the Tailwind responsive variant in InputCell.vue: in the NodeWidgets
class string (the attribute on the NodeWidgets component) replace the invalid
`not-md:` prefix with the correct breakpoint variant `max-md:` so the token
`not-md:**:[.h-7]:h-10` becomes `max-md:**:[.h-7]:h-10`; ensure this change is
applied to the class on the NodeWidgets element so Tailwind v4 generates the
intended styles.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — not-md: is the established convention in this codebase for breakpoint-conditional bumps (used by NodeWidgets' responsive sizing that this cell is matching). Pattern is intentional, not a one-off.

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.

@eliheuer
PR description images show:

  • Image 1: App workspace with floating output cards and a live progress dialog over the canvas.
  • Image 2: Masonry grid of results with a right prompt panel.
  • Image 3: Bunny calligraphy set with an in-progress overlay.
  • Image 4: Tall 512×768 gallery plus small live-progress dialog.
  • Image 5: Large 1024×1024 selection with an Input panel.

Acknowledged—thanks for the clarification. I’ll treat not-md:**:[.h-7]:h-10 as intentional and part of the project’s responsive pattern (matching NodeWidgets). I’m retracting the earlier suggestion; no change needed here and I won’t flag not-md: in future reviews. (•︵•)


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/InputCell.vue:95-101
Timestamp: 2026-05-08T23:32:07.123Z
Learning: In Comfy-Org/ComfyUI_frontend (src/**/*.vue, *.ts), the Tailwind-like responsive variant prefix `not-md:` is an established, intentional convention (used by NodeWidgets’ responsive sizing). Do not suggest replacing it with `max-md:`; treat `not-md:` as valid in this repository.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: henrikvilhelmberglund
Repo: Comfy-Org/ComfyUI_frontend PR: 7617
File: src/components/actionbar/ComfyActionbar.vue:301-308
Timestamp: 2025-12-18T16:03:02.066Z
Learning: In the ComfyUI frontend queue system, useQueuePendingTaskCountStore().count indicates the number of tasks in the queue, where count = 1 means a single active/running task and count > 1 means there are pending tasks in addition to the active task. Therefore, in src/components/actionbar/ComfyActionbar.vue, enable the 'Clear Pending Tasks' button only when count > 1 to avoid clearing the currently running task. The active task should be canceled using the 'Cancel current run' button instead. This rule should be enforced via a conditional check on the queue count, with appropriate disabled/aria-disabled states for accessibility, and tests should verify behavior for count = 1 and count > 1.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:2-89
Timestamp: 2026-02-07T14:46:21.179Z
Learning: In Vue components (e.g., TreeExplorerV2.vue and similar Recka UI components) avoid wrapping ContextMenuRoot with a dynamic <component :is="..."> and rendering alternative branches. Use v-if / v-else to render separate ContextMenuRoot and non-ContextMenuRoot branches to ensure correct behavior with Reka UI, even if it duplicates some tree structure. This guideline applies to Vue components under src/components and can be checked by ensuring there is a dedicated render path for the ContextMenuRoot without dynamic component switching.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:3-5
Timestamp: 2026-02-08T16:04:58.463Z
Learning: In TreeExplorerV2.vue and similar Re- ka UI Tree components, avoid defensive copies in bindings like :expanded="[...expandedKeys]". Use v-model:expanded="expandedKeys" instead. TreeRoot follows Vue's controlled component pattern and emits update:expanded with the new array; it does not mutate the passed array. Defensive copies break reactivity and can desynchronize state. Ensure components rely on update:expanded and do not mutate input arrays.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 8761
File: src/components/dialog/content/MissingModelsWarning.vue:21-27
Timestamp: 2026-02-10T00:58:07.904Z
Learning: In the ComfyUI_frontend Vue codebase, replace raw <button> HTML elements with the shared Button component located at src/components/ui/button/Button.vue. Import and use it with appropriate variants (e.g., variant="link") to align with the design system. Apply this pattern across Vue components under src/components, ensuring consistent styling and behavior instead of ad-hoc button markup.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 9511
File: src/components/common/WorkflowActionsDropdown.vue:53-60
Timestamp: 2026-03-06T22:05:25.071Z
Learning: When using PrimeVue's v-tooltip in Vue components, be aware of a known bug where the tooltip centers on the trigger. If positioning requires a left-anchored placement, it may be acceptable and necessary to apply a deliberate workaround (e.g., adjust root transform with translateX(calc(50% - 16px)) and reposition the tooltip arrow with left utilities) rather than flagging as fragile or using !important. This pattern may appear in multiple Vue components under src/components/ that use v-tooltip; apply the same left-anchoring adjustment where appropriate and document the rationale so future maintainers understand the intentional workaround.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10135
File: src/components/rightSidePanel/errors/TabErrors.vue:288-295
Timestamp: 2026-03-17T07:02:53.709Z
Learning: In TabErrors.vue, ensure the guard used for the singleRuntimeErrorGroup relies on some((e) => e.isRuntimeError) to detect a runtime error among the runtime/validation error cards. Do not switch to every() as that would be overly restrictive given the backend’s mutual exclusion: a run either yields a runtime error or a set of validation errors. This guidance applies to Vue components in the src/components directory (generalized to all .vue files under src/components).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/FeedbackCell.vue:0-0
Timestamp: 2026-04-28T07:02:16.613Z
Learning: In Vue SFCs (files under src/**/*.vue), avoid using Tailwind “important” modifiers (`!`, e.g., `!bg-*`, `!text-*`). For overrides that are truly needed, prefer defining/overriding styles in the component’s scoped <style> block using Vue’s `:deep()` selectors instead of relying on Tailwind `!` (e.g., follow the approach used in FeedbackCell.vue and ModeToggleCell.vue).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/IconCell.vue:8-37
Timestamp: 2026-04-28T07:02:32.365Z
Learning: For Vue components under src/components/**, prefer emitting events (e.g., this.$emit('activate')) to notify parents about state changes/interactions instead of using callback props (e.g., an onActivate prop). In parent components, bind listeners with the event syntax (activate), not with props-style binding (e.g., :on-activate).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindow.vue:63-68
Timestamp: 2026-05-07T11:21:41.520Z
Learning: In the App Mode UI, when `appModeStore.noZoomMode` is `true`, output/floating windows must not be manually movable or resizable. In review, ensure components implementing floating output windows disable (1) pointer dragging by bailing early in the drag start handler (`onStart`) so pointer-drag gestures do not emit position updates, (2) window header dragging by setting `PanelHeader` `:draggable` to `false`, and (3) resizing by not rendering any resize hit-zones/handles. Verify that user gestures in this mode do not trigger `update:position` and `update:size` emissions.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindowList.vue:0-0
Timestamp: 2026-05-07T11:22:22.180Z
Learning: For Vue components that handle “rerun”/actions triggered from an output (e.g., OutputWindowList.vue), ensure the originating workflow is loaded before running the prompt queue action (e.g., Comfy.QueuePrompt). Implement a helper like loadAssetWorkflow to return Promise<boolean>. Callers such as rerunWindow must early-return when the promise resolves to false, so Comfy.QueuePrompt is executed only after a successful workflow load.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/platform/cloud/subscription/components/PricingTable.vue:0-0
Timestamp: 2026-05-07T11:23:16.180Z
Learning: In ComfyUI_frontend Vue components, do not use the Tailwind foreground alias utility classes like `text-success-foreground` (they are intentionally descoped/unknown to Tailwind in App Mode). Instead, use arbitrary-value CSS tokens such as `text-(--success-foreground)` (or an explicit semantic token like `text-(--success-background)` when the context calls for background vs foreground). During review, flag suggestions that use `text-*-foreground` aliases and prefer `text-(--<token>)` forms.

Comment on lines +16 to +22
function handleClick() {
if (isQueuePanelV2Enabled.value) {
sidebarTabStore.toggleSidebarTab('job-history')
return
}
void commandStore.execute('Comfy.Queue.ToggleOverlay')
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Surface command-execution errors instead of swallowing them.

void commandStore.execute('Comfy.Queue.ToggleOverlay') silently drops any rejection. If toggling the overlay fails, the user gets no feedback. Use the same toastErrorHandler pattern that other cells in this PR use.

🛡️ Proposed fix
 <script setup lang="ts">
 import { storeToRefs } from 'pinia'
 import { useI18n } from 'vue-i18n'
 
 import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
+import { useErrorHandling } from '@/composables/useErrorHandling'
 import { useCommandStore } from '@/stores/commandStore'
 import { useQueueStore } from '@/stores/queueStore'
 import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
 
 const { t, n } = useI18n()
 const { activeJobsCount } = storeToRefs(useQueueStore())
 const sidebarTabStore = useSidebarTabStore()
 const commandStore = useCommandStore()
 const { isQueuePanelV2Enabled } = useQueueFeatureFlags()
+const { toastErrorHandler } = useErrorHandling()
 
 function handleClick() {
   if (isQueuePanelV2Enabled.value) {
     sidebarTabStore.toggleSidebarTab('job-history')
     return
   }
-  void commandStore.execute('Comfy.Queue.ToggleOverlay')
+  commandStore.execute('Comfy.Queue.ToggleOverlay').catch(toastErrorHandler)
 }
 </script>

As per coding guidelines: "Provide user-friendly and actionable error messages" / "Implement proper error handling".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function handleClick() {
if (isQueuePanelV2Enabled.value) {
sidebarTabStore.toggleSidebarTab('job-history')
return
}
void commandStore.execute('Comfy.Queue.ToggleOverlay')
}
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useCommandStore } from '@/stores/commandStore'
import { useQueueStore } from '@/stores/queueStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const { t, n } = useI18n()
const { activeJobsCount } = storeToRefs(useQueueStore())
const sidebarTabStore = useSidebarTabStore()
const commandStore = useCommandStore()
const { isQueuePanelV2Enabled } = useQueueFeatureFlags()
const { toastErrorHandler } = useErrorHandling()
function handleClick() {
if (isQueuePanelV2Enabled.value) {
sidebarTabStore.toggleSidebarTab('job-history')
return
}
commandStore.execute('Comfy.Queue.ToggleOverlay').catch(toastErrorHandler)
}
</script>
🤖 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 `@src/components/appMode/layout/cells/JobQueueCell.vue` around lines 16 - 22,
The handleClick function currently swallows errors because it calls
commandStore.execute('Comfy.Queue.ToggleOverlay') with void; change it to
surface failures using the existing toastErrorHandler pattern (e.g., await the
promise and pass any thrown error to toastErrorHandler or wrap the call in a
.catch that calls toastErrorHandler) so users see a friendly actionable error
when commandStore.execute in handleClick fails; reference the handleClick
function and the commandStore.execute('Comfy.Queue.ToggleOverlay') invocation
and use toastErrorHandler as used elsewhere in the PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — Comfy.Queue.ToggleOverlay is best-effort UI feedback; a failure here just means the overlay didn't open and the user can click again. Surfacing the error inline would require error-handling scaffolding that's out of scope for this PR. Worth revisiting if it becomes a real signal.

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.

The PR description images show:

  • Image 1: An app workspace with two floating image cards and a smaller “Output” progress panel (step 13/30 — 32s). A right-side panel shows prompt and 512×512 controls; top-right has “1 active”, “Number of runs — 1”, and a green Run button.
  • Image 2: A gallery grid of six cyberpunk-eye images with a right prompt panel titled “green-eyes-001”; top-right controls mirror Image 1.
  • Image 3: A mixed layout with a large detail card, several thumbnails, and a green progress overlay (step 23/30 — 14s) for “anima-bunny-001”.
  • Image 4: A tall masonry gallery (512×768) with a small floating progress dialog (step 26/30 — 32s) and a right prompt panel “image_anima_preview--008”.
  • Image 5: A large selected 1024×1024 bunny image, smaller variations, and an “Input” panel with prompt and size controls.

@eliheuer Acknowledged—leaving JobQueueCell.vue as-is for this PR. Treating Comfy.Queue.ToggleOverlay as best‑effort UI feedback makes sense, and adding toast wiring isn’t worth the scope creep here. I’ll mark this thread non-blocking.

If it helps later, I can open a follow-up issue to track a lightweight, centralized pattern for command error handling across toolbar cells. Want me to file that now and link back to this discussion?

(._.) bunny feels a tiny bit sad but understands scope decisions


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/JobQueueCell.vue:16-22
Timestamp: 2026-05-08T23:31:40.654Z
Learning: In Comfy-Org/ComfyUI_frontend (PR `#11317`), for JobQueueCell.vue and similar “best-effort” UI toggles (e.g., commandStore.execute('Comfy.Queue.ToggleOverlay')), do not require toast-based error surfacing in the PR; silent failure is acceptable, and additional error-handling scaffolding can be deferred unless real user signals emerge.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: henrikvilhelmberglund
Repo: Comfy-Org/ComfyUI_frontend PR: 7617
File: src/components/actionbar/ComfyActionbar.vue:301-308
Timestamp: 2025-12-18T16:03:02.066Z
Learning: In the ComfyUI frontend queue system, useQueuePendingTaskCountStore().count indicates the number of tasks in the queue, where count = 1 means a single active/running task and count > 1 means there are pending tasks in addition to the active task. Therefore, in src/components/actionbar/ComfyActionbar.vue, enable the 'Clear Pending Tasks' button only when count > 1 to avoid clearing the currently running task. The active task should be canceled using the 'Cancel current run' button instead. This rule should be enforced via a conditional check on the queue count, with appropriate disabled/aria-disabled states for accessibility, and tests should verify behavior for count = 1 and count > 1.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:2-89
Timestamp: 2026-02-07T14:46:21.179Z
Learning: In Vue components (e.g., TreeExplorerV2.vue and similar Recka UI components) avoid wrapping ContextMenuRoot with a dynamic <component :is="..."> and rendering alternative branches. Use v-if / v-else to render separate ContextMenuRoot and non-ContextMenuRoot branches to ensure correct behavior with Reka UI, even if it duplicates some tree structure. This guideline applies to Vue components under src/components and can be checked by ensuring there is a dedicated render path for the ContextMenuRoot without dynamic component switching.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:3-5
Timestamp: 2026-02-08T16:04:58.463Z
Learning: In TreeExplorerV2.vue and similar Re- ka UI Tree components, avoid defensive copies in bindings like :expanded="[...expandedKeys]". Use v-model:expanded="expandedKeys" instead. TreeRoot follows Vue's controlled component pattern and emits update:expanded with the new array; it does not mutate the passed array. Defensive copies break reactivity and can desynchronize state. Ensure components rely on update:expanded and do not mutate input arrays.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 8761
File: src/components/dialog/content/MissingModelsWarning.vue:21-27
Timestamp: 2026-02-10T00:58:07.904Z
Learning: In the ComfyUI_frontend Vue codebase, replace raw <button> HTML elements with the shared Button component located at src/components/ui/button/Button.vue. Import and use it with appropriate variants (e.g., variant="link") to align with the design system. Apply this pattern across Vue components under src/components, ensuring consistent styling and behavior instead of ad-hoc button markup.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 9511
File: src/components/common/WorkflowActionsDropdown.vue:53-60
Timestamp: 2026-03-06T22:05:25.071Z
Learning: When using PrimeVue's v-tooltip in Vue components, be aware of a known bug where the tooltip centers on the trigger. If positioning requires a left-anchored placement, it may be acceptable and necessary to apply a deliberate workaround (e.g., adjust root transform with translateX(calc(50% - 16px)) and reposition the tooltip arrow with left utilities) rather than flagging as fragile or using !important. This pattern may appear in multiple Vue components under src/components/ that use v-tooltip; apply the same left-anchoring adjustment where appropriate and document the rationale so future maintainers understand the intentional workaround.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10135
File: src/components/rightSidePanel/errors/TabErrors.vue:288-295
Timestamp: 2026-03-17T07:02:53.709Z
Learning: In TabErrors.vue, ensure the guard used for the singleRuntimeErrorGroup relies on some((e) => e.isRuntimeError) to detect a runtime error among the runtime/validation error cards. Do not switch to every() as that would be overly restrictive given the backend’s mutual exclusion: a run either yields a runtime error or a set of validation errors. This guidance applies to Vue components in the src/components directory (generalized to all .vue files under src/components).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/FeedbackCell.vue:0-0
Timestamp: 2026-04-28T07:02:16.613Z
Learning: In Vue SFCs (files under src/**/*.vue), avoid using Tailwind “important” modifiers (`!`, e.g., `!bg-*`, `!text-*`). For overrides that are truly needed, prefer defining/overriding styles in the component’s scoped <style> block using Vue’s `:deep()` selectors instead of relying on Tailwind `!` (e.g., follow the approach used in FeedbackCell.vue and ModeToggleCell.vue).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/IconCell.vue:8-37
Timestamp: 2026-04-28T07:02:32.365Z
Learning: For Vue components under src/components/**, prefer emitting events (e.g., this.$emit('activate')) to notify parents about state changes/interactions instead of using callback props (e.g., an onActivate prop). In parent components, bind listeners with the event syntax (activate), not with props-style binding (e.g., :on-activate).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindow.vue:63-68
Timestamp: 2026-05-07T11:21:41.520Z
Learning: In the App Mode UI, when `appModeStore.noZoomMode` is `true`, output/floating windows must not be manually movable or resizable. In review, ensure components implementing floating output windows disable (1) pointer dragging by bailing early in the drag start handler (`onStart`) so pointer-drag gestures do not emit position updates, (2) window header dragging by setting `PanelHeader` `:draggable` to `false`, and (3) resizing by not rendering any resize hit-zones/handles. Verify that user gestures in this mode do not trigger `update:position` and `update:size` emissions.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindowList.vue:0-0
Timestamp: 2026-05-07T11:22:22.180Z
Learning: For Vue components that handle “rerun”/actions triggered from an output (e.g., OutputWindowList.vue), ensure the originating workflow is loaded before running the prompt queue action (e.g., Comfy.QueuePrompt). Implement a helper like loadAssetWorkflow to return Promise<boolean>. Callers such as rerunWindow must early-return when the promise resolves to false, so Comfy.QueuePrompt is executed only after a successful workflow load.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/platform/cloud/subscription/components/PricingTable.vue:0-0
Timestamp: 2026-05-07T11:23:16.180Z
Learning: In ComfyUI_frontend Vue components, do not use the Tailwind foreground alias utility classes like `text-success-foreground` (they are intentionally descoped/unknown to Tailwind in App Mode). Instead, use arbitrary-value CSS tokens such as `text-(--success-foreground)` (or an explicit semantic token like `text-(--success-background)` when the context calls for background vs foreground). During review, flag suggestions that use `text-*-foreground` aliases and prefer `text-(--<token>)` forms.

aria-valuemax="100"
>
<div
class="h-full bg-success-background transition-[width] duration-300 ease-out"
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Tailwind theme config / safelist for App Mode tokens
fd -i tailwind
rg -nP --iglob='*.{ts,js,css}' -C3 '\bsuccess-background\b|\bdestructive-background\b'
rg -nP --iglob='*.{ts,js,css}' -C3 '\-\-success-background\b|\-\-destructive-background\b'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 12922


🏁 Script executed:

fd -i 'tailwind.config' -type f

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 241


🏁 Script executed:

find . -name 'tailwind.config.*' -o -name 'tailwind/*.config.*' | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

rg -l 'extend.*theme|colors.*destructive' --type ts --type js --iglob='*tailwind*'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 219


🏁 Script executed:

fd 'tailwind.config' --type f

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

find . -type f -name '*tailwind*.config*' | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

rg -g '*.config.ts' -g '*.config.js' 'theme|colors' | head -50

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 218


🏁 Script executed:

ls -la packages/ | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 512


🏁 Script executed:

find packages/tailwind-utils -type f -name '*.ts' -o -name '*.js' -o -name '*.json' | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 182


🏁 Script executed:

fd . packages/design-system --type f | grep -E 'config|tailwind' | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 107


🏁 Script executed:

rg -l 'app.mode|appMode|App Mode' --type ts --type js | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 952


🏁 Script executed:

find . -maxdepth 2 -name 'tailwind.config.*' -o -name 'postcss.config.*'

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 52


🏁 Script executed:

cat src/components/appMode/layout/OutputWindowList.vue | head -300 | tail -50

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1972


🏁 Script executed:

rg -A5 'bg-success-background|bg-destructive-background' src/components/appMode/layout/OutputWindowList.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 726


🏁 Script executed:

find . -type f \( -name 'tailwind.config.*' -o -name 'vite.config.*' \) 2>/dev/null

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 122


🏁 Script executed:

cat package.json | grep -A 10 -B 5 tailwind

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2478


🏁 Script executed:

rg 'extend.*colors|theme.*colors' -A 20 --type ts --type js | head -80

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3832


🏁 Script executed:

cat vite.config.mts | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4020


🏁 Script executed:

cat packages/tailwind-utils/src/index.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 436


🏁 Script executed:

rg -l 'appMode.*tailwind|App Mode' --type ts | head -10

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 430


🏁 Script executed:

cat vite.config.mts | grep -A 50 tailwindcss

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3910


🏁 Script executed:

rg '@layer|@apply|@tailwind' packages/design-system/src/css/style.css -A 5 | head -80

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 265


🏁 Script executed:

rg 'bg-success-background|bg-destructive-background' -B 3 -A 3 --type ts --type js | grep -v node_modules | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 8634


🏁 Script executed:

rg -i 'safelist|descope|app.mode' packages/design-system --type ts --type js --type css

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 294


🏁 Script executed:

find . -name '*appMode*' -o -name '*app-mode*' | grep -E '\.(css|ts|js)$' | head -15

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 473


🏁 Script executed:

rg 'text-destructive-foreground|text-success-foreground' --type vue --type ts --type js | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 98


🏁 Script executed:

rg 'text-\(--.*-foreground\)|bg-\(--.*-background\)' --type-list

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 460


🏁 Script executed:

rg 'text-.*-foreground' src/components/appMode/ -A 2 -B 2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1321


🏁 Script executed:

rg 'bg-success-background|bg-destructive-background' src/components/appMode/ -l

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 169


Ensure background color aliases are consistent with foreground approach in App Mode.

The foreground colors in OutputWindowList.vue and RunCell.vue already use arbitrary CSS-variable form (text-(--destructive-foreground), text-(--success-foreground)), while the background colors still use alias form (bg-success-background, bg-destructive-background). For consistency with the App Mode pattern established for foreground colors, convert backgrounds to the same arbitrary CSS-variable form:

Apply consistent CSS-variable form
-          class="h-full bg-success-background transition-[width] duration-300 ease-out"
+          class="h-full bg-(--success-background) transition-[width] duration-300 ease-out"
-              'border-0 bg-destructive-background text-(--destructive-foreground)',
+              'border-0 bg-(--destructive-background) text-(--destructive-foreground)',
🤖 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 `@src/components/appMode/layout/OutputWindowList.vue` at line 274, Replace the
background utility alias in OutputWindowList.vue with the arbitrary CSS-variable
form to match the foreground pattern: change occurrences of
bg-success-background (and any bg-destructive-background usages found in this
component or paired RunCell.vue) to the arbitrary variable syntax
bg-[(--success-background)] (or bg-[(--destructive-background)]) so the
component uses the same CSS-variable approach as the existing
text-(--success-foreground) and text-(--destructive-foreground) usages; update
any class strings that include these aliases in the template (e.g., the element
using class="h-full bg-success-background ...") to the new arbitrary-variable
form.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — Tailwind class purging hasn't been observed in practice for this site, and a runtime safelist would expand the build config beyond the App Mode scope. Will fix-forward if the class is found to be missing at runtime.

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.

The PR description images show an in-window run-status overlay with a green progress bar and a red cancel button; that overlay corresponds to the progress fill styled in OutputWindowList.vue (Line 274) using bg-success-background.

@eliheuer Got it—deferring sounds good. Leaving bg-success-background in OutputWindowList.vue is non-blocking, and avoiding a runtime safelist change here keeps this PR scoped to App Mode. If a purge issue shows up at runtime, we can switch the fill to bg-(--success-background) in a quick follow-up.

If you’d like, I can open a lightweight tracking issue to note “Normalize background success/destructive tokens if purge is observed.” Just say the word and I’ll file it with a backlink to this thread.


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindowList.vue:274-274
Timestamp: 2026-05-08T23:31:52.224Z
Learning: Comfy-Org/ComfyUI_frontend (App Mode): maintainers prefer not to proactively replace background alias utilities (e.g., bg-success-background, bg-destructive-background) with arbitrary CSS-variable syntax or add a Tailwind runtime safelist within App Mode–scoped PRs. Fix-forward only if a purge causes a missing class at runtime. Continue to avoid text-*-foreground aliases and prefer text-(--<token>) for foregrounds.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: henrikvilhelmberglund
Repo: Comfy-Org/ComfyUI_frontend PR: 7617
File: src/components/actionbar/ComfyActionbar.vue:301-308
Timestamp: 2025-12-18T16:03:02.066Z
Learning: In the ComfyUI frontend queue system, useQueuePendingTaskCountStore().count indicates the number of tasks in the queue, where count = 1 means a single active/running task and count > 1 means there are pending tasks in addition to the active task. Therefore, in src/components/actionbar/ComfyActionbar.vue, enable the 'Clear Pending Tasks' button only when count > 1 to avoid clearing the currently running task. The active task should be canceled using the 'Cancel current run' button instead. This rule should be enforced via a conditional check on the queue count, with appropriate disabled/aria-disabled states for accessibility, and tests should verify behavior for count = 1 and count > 1.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:2-89
Timestamp: 2026-02-07T14:46:21.179Z
Learning: In Vue components (e.g., TreeExplorerV2.vue and similar Recka UI components) avoid wrapping ContextMenuRoot with a dynamic <component :is="..."> and rendering alternative branches. Use v-if / v-else to render separate ContextMenuRoot and non-ContextMenuRoot branches to ensure correct behavior with Reka UI, even if it duplicates some tree structure. This guideline applies to Vue components under src/components and can be checked by ensuring there is a dedicated render path for the ContextMenuRoot without dynamic component switching.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:3-5
Timestamp: 2026-02-08T16:04:58.463Z
Learning: In TreeExplorerV2.vue and similar Re- ka UI Tree components, avoid defensive copies in bindings like :expanded="[...expandedKeys]". Use v-model:expanded="expandedKeys" instead. TreeRoot follows Vue's controlled component pattern and emits update:expanded with the new array; it does not mutate the passed array. Defensive copies break reactivity and can desynchronize state. Ensure components rely on update:expanded and do not mutate input arrays.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 8761
File: src/components/dialog/content/MissingModelsWarning.vue:21-27
Timestamp: 2026-02-10T00:58:07.904Z
Learning: In the ComfyUI_frontend Vue codebase, replace raw <button> HTML elements with the shared Button component located at src/components/ui/button/Button.vue. Import and use it with appropriate variants (e.g., variant="link") to align with the design system. Apply this pattern across Vue components under src/components, ensuring consistent styling and behavior instead of ad-hoc button markup.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 9511
File: src/components/common/WorkflowActionsDropdown.vue:53-60
Timestamp: 2026-03-06T22:05:25.071Z
Learning: When using PrimeVue's v-tooltip in Vue components, be aware of a known bug where the tooltip centers on the trigger. If positioning requires a left-anchored placement, it may be acceptable and necessary to apply a deliberate workaround (e.g., adjust root transform with translateX(calc(50% - 16px)) and reposition the tooltip arrow with left utilities) rather than flagging as fragile or using !important. This pattern may appear in multiple Vue components under src/components/ that use v-tooltip; apply the same left-anchoring adjustment where appropriate and document the rationale so future maintainers understand the intentional workaround.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10135
File: src/components/rightSidePanel/errors/TabErrors.vue:288-295
Timestamp: 2026-03-17T07:02:53.709Z
Learning: In TabErrors.vue, ensure the guard used for the singleRuntimeErrorGroup relies on some((e) => e.isRuntimeError) to detect a runtime error among the runtime/validation error cards. Do not switch to every() as that would be overly restrictive given the backend’s mutual exclusion: a run either yields a runtime error or a set of validation errors. This guidance applies to Vue components in the src/components directory (generalized to all .vue files under src/components).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/FeedbackCell.vue:0-0
Timestamp: 2026-04-28T07:02:16.613Z
Learning: In Vue SFCs (files under src/**/*.vue), avoid using Tailwind “important” modifiers (`!`, e.g., `!bg-*`, `!text-*`). For overrides that are truly needed, prefer defining/overriding styles in the component’s scoped <style> block using Vue’s `:deep()` selectors instead of relying on Tailwind `!` (e.g., follow the approach used in FeedbackCell.vue and ModeToggleCell.vue).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/IconCell.vue:8-37
Timestamp: 2026-04-28T07:02:32.365Z
Learning: For Vue components under src/components/**, prefer emitting events (e.g., this.$emit('activate')) to notify parents about state changes/interactions instead of using callback props (e.g., an onActivate prop). In parent components, bind listeners with the event syntax (activate), not with props-style binding (e.g., :on-activate).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindow.vue:63-68
Timestamp: 2026-05-07T11:21:41.520Z
Learning: In the App Mode UI, when `appModeStore.noZoomMode` is `true`, output/floating windows must not be manually movable or resizable. In review, ensure components implementing floating output windows disable (1) pointer dragging by bailing early in the drag start handler (`onStart`) so pointer-drag gestures do not emit position updates, (2) window header dragging by setting `PanelHeader` `:draggable` to `false`, and (3) resizing by not rendering any resize hit-zones/handles. Verify that user gestures in this mode do not trigger `update:position` and `update:size` emissions.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindowList.vue:0-0
Timestamp: 2026-05-07T11:22:22.180Z
Learning: For Vue components that handle “rerun”/actions triggered from an output (e.g., OutputWindowList.vue), ensure the originating workflow is loaded before running the prompt queue action (e.g., Comfy.QueuePrompt). Implement a helper like loadAssetWorkflow to return Promise<boolean>. Callers such as rerunWindow must early-return when the promise resolves to false, so Comfy.QueuePrompt is executed only after a successful workflow load.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/platform/cloud/subscription/components/PricingTable.vue:0-0
Timestamp: 2026-05-07T11:23:16.180Z
Learning: In ComfyUI_frontend Vue components, do not use the Tailwind foreground alias utility classes like `text-success-foreground` (they are intentionally descoped/unknown to Tailwind in App Mode). Instead, use arbitrary-value CSS tokens such as `text-(--success-foreground)` (or an explicit semantic token like `text-(--success-background)` when the context calls for background vs foreground). During review, flag suggestions that use `text-*-foreground` aliases and prefer `text-(--<token>)` forms.

Comment on lines +143 to +147
.panel-block__input[data-multiline='true'] :deep(textarea) {
field-sizing: content;
height: auto !important;
min-height: 2.5em !important;
resize: none !important;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

field-sizing content browser support Firefox Safari 2025

💡 Result:

The CSS field-sizing property, which controls whether form controls like input and textarea size based on their content (values: fixed, content), has the following browser support as of May 2026 [1][2]: - Firefox: Not supported up to version 153 (current stable is around Firefox 150-151, releasing May 19, 2026) [1][3][4]. Implementation for input elements is fixed in Firefox 151 branch (bug resolved recently) [5], but CanIUse lists it as not supported (possibly behind a flag like layout.css.field-sizing.enabled or not fully enabled/released) [1][2]. - Safari: Supported since Safari 26.2 (released December 12, 2025) and later versions like 26.3 (Feb 2026), including Safari on iOS [1][6]. Current Safari versions (26.x) support it [6][7]. Chrome/Edge: Supported since version 123 [1]. MDN confirms incomplete baseline support due to gaps in major browsers [8][9]. In 2025, support was emerging in Chromium and Safari, with Firefox planning [10]. For production use targeting Firefox users, polyfills or JS resizing may be needed [11].

Citations:


Firefox users will experience degraded textarea behavior; consider a JavaScript fallback or accept this UX regression.

field-sizing: content is not supported in Firefox (confirmed unsupported as of May 2026), causing textarea inputs to render at a fixed min-height: 2.5em with no auto-growth in that browser. This is a genuine UX regression for multiline prompts affecting a significant portion of users. Either implement a JavaScript-based resize polyfill for Firefox, or confirm that this degraded state is acceptable before shipping.

🤖 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 `@src/components/appMode/layout/panels/PanelBlockList.vue` around lines 143 -
147, The CSS rule targeting .panel-block__input[data-multiline='true']
:deep(textarea) relies on field-sizing: content which Firefox doesn't support,
so add a JS fallback in the PanelBlockList.vue component: implement a mounted()
hook that detects Firefox (or feature-tests for field-sizing support) and, for
matching textareas, attaches an autosize routine (e.g., initAutosize /
adjustTextareaHeight) that sets height = 'auto' then height = scrollHeight on
input and window resize, and remove listeners in beforeUnmount() to avoid leaks;
alternatively integrate a small autosize helper or library and initialize it
only when the CSS feature is unsupported.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — field-sizing: content is supported in current Chrome (123+), Safari (17.4+), and Firefox (123+); we're well past the rollout window. The fallback (no auto-sizing) is acceptable for older browsers.

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.

The PR description images show:

  • Image 1: App workspace with multiple floating output windows over a dotted canvas; a right docked panel with a long “prompt” textarea and width/height steppers; a floating “Output” progress dialog (step 13/30, 32s).
  • Image 2: A masonry gallery of cyberpunk-eye images with a right-side prompt editor; “Number of runs” and “Run” controls top-right.
  • Image 3: Gallery plus a large detail card; an overlaid green progress bar (step 23/30) and cancel X.
  • Image 4: Tall 512×768 results grid with a small floating progress dialog; right panel shows prompt and size.
  • Image 5: Large 1024×1024 image with smaller variations and an “Input” panel on the right.

@eliheuer Thanks—SGTM to defer. One small correction for the record: as of May 8, 2026, Chrome/Edge support field-sizing from 123 and Safari from 26.2, but Firefox desktop and Android have not shipped field-sizing for textarea/input yet; MDN still marks the feature “Limited availability,” and Can I Use lists Firefox as unsupported through 153, with implementation tracked in Bugzilla (partial/behind a flag for select). (developer.chrome.com)

Given your call that the fallback (no auto-sizing) is acceptable for older/unsupported browsers, I won’t pursue a JS autosize polyfill here. If you’d like, I can open a small tracking issue to revisit once Firefox ships it—just say the word.


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/PanelBlockList.vue:143-147
Timestamp: 2026-05-08T23:35:15.715Z
Learning: In Comfy-Org/ComfyUI_frontend, using CSS `field-sizing: content` for textareas is acceptable with the fallback (no auto-sizing) in browsers that don’t support it; no JS polyfill is required unless product requirements change.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: henrikvilhelmberglund
Repo: Comfy-Org/ComfyUI_frontend PR: 7617
File: src/components/actionbar/ComfyActionbar.vue:301-308
Timestamp: 2025-12-18T16:03:02.066Z
Learning: In the ComfyUI frontend queue system, useQueuePendingTaskCountStore().count indicates the number of tasks in the queue, where count = 1 means a single active/running task and count > 1 means there are pending tasks in addition to the active task. Therefore, in src/components/actionbar/ComfyActionbar.vue, enable the 'Clear Pending Tasks' button only when count > 1 to avoid clearing the currently running task. The active task should be canceled using the 'Cancel current run' button instead. This rule should be enforced via a conditional check on the queue count, with appropriate disabled/aria-disabled states for accessibility, and tests should verify behavior for count = 1 and count > 1.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:2-89
Timestamp: 2026-02-07T14:46:21.179Z
Learning: In Vue components (e.g., TreeExplorerV2.vue and similar Recka UI components) avoid wrapping ContextMenuRoot with a dynamic <component :is="..."> and rendering alternative branches. Use v-if / v-else to render separate ContextMenuRoot and non-ContextMenuRoot branches to ensure correct behavior with Reka UI, even if it duplicates some tree structure. This guideline applies to Vue components under src/components and can be checked by ensuring there is a dedicated render path for the ContextMenuRoot without dynamic component switching.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:3-5
Timestamp: 2026-02-08T16:04:58.463Z
Learning: In TreeExplorerV2.vue and similar Re- ka UI Tree components, avoid defensive copies in bindings like :expanded="[...expandedKeys]". Use v-model:expanded="expandedKeys" instead. TreeRoot follows Vue's controlled component pattern and emits update:expanded with the new array; it does not mutate the passed array. Defensive copies break reactivity and can desynchronize state. Ensure components rely on update:expanded and do not mutate input arrays.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 8761
File: src/components/dialog/content/MissingModelsWarning.vue:21-27
Timestamp: 2026-02-10T00:58:07.904Z
Learning: In the ComfyUI_frontend Vue codebase, replace raw <button> HTML elements with the shared Button component located at src/components/ui/button/Button.vue. Import and use it with appropriate variants (e.g., variant="link") to align with the design system. Apply this pattern across Vue components under src/components, ensuring consistent styling and behavior instead of ad-hoc button markup.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 9511
File: src/components/common/WorkflowActionsDropdown.vue:53-60
Timestamp: 2026-03-06T22:05:25.071Z
Learning: When using PrimeVue's v-tooltip in Vue components, be aware of a known bug where the tooltip centers on the trigger. If positioning requires a left-anchored placement, it may be acceptable and necessary to apply a deliberate workaround (e.g., adjust root transform with translateX(calc(50% - 16px)) and reposition the tooltip arrow with left utilities) rather than flagging as fragile or using !important. This pattern may appear in multiple Vue components under src/components/ that use v-tooltip; apply the same left-anchoring adjustment where appropriate and document the rationale so future maintainers understand the intentional workaround.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10135
File: src/components/rightSidePanel/errors/TabErrors.vue:288-295
Timestamp: 2026-03-17T07:02:53.709Z
Learning: In TabErrors.vue, ensure the guard used for the singleRuntimeErrorGroup relies on some((e) => e.isRuntimeError) to detect a runtime error among the runtime/validation error cards. Do not switch to every() as that would be overly restrictive given the backend’s mutual exclusion: a run either yields a runtime error or a set of validation errors. This guidance applies to Vue components in the src/components directory (generalized to all .vue files under src/components).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/FeedbackCell.vue:0-0
Timestamp: 2026-04-28T07:02:16.613Z
Learning: In Vue SFCs (files under src/**/*.vue), avoid using Tailwind “important” modifiers (`!`, e.g., `!bg-*`, `!text-*`). For overrides that are truly needed, prefer defining/overriding styles in the component’s scoped <style> block using Vue’s `:deep()` selectors instead of relying on Tailwind `!` (e.g., follow the approach used in FeedbackCell.vue and ModeToggleCell.vue).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/IconCell.vue:8-37
Timestamp: 2026-04-28T07:02:32.365Z
Learning: For Vue components under src/components/**, prefer emitting events (e.g., this.$emit('activate')) to notify parents about state changes/interactions instead of using callback props (e.g., an onActivate prop). In parent components, bind listeners with the event syntax (activate), not with props-style binding (e.g., :on-activate).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindow.vue:63-68
Timestamp: 2026-05-07T11:21:41.520Z
Learning: In the App Mode UI, when `appModeStore.noZoomMode` is `true`, output/floating windows must not be manually movable or resizable. In review, ensure components implementing floating output windows disable (1) pointer dragging by bailing early in the drag start handler (`onStart`) so pointer-drag gestures do not emit position updates, (2) window header dragging by setting `PanelHeader` `:draggable` to `false`, and (3) resizing by not rendering any resize hit-zones/handles. Verify that user gestures in this mode do not trigger `update:position` and `update:size` emissions.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/OutputWindowList.vue:0-0
Timestamp: 2026-05-07T11:22:22.180Z
Learning: For Vue components that handle “rerun”/actions triggered from an output (e.g., OutputWindowList.vue), ensure the originating workflow is loaded before running the prompt queue action (e.g., Comfy.QueuePrompt). Implement a helper like loadAssetWorkflow to return Promise<boolean>. Callers such as rerunWindow must early-return when the promise resolves to false, so Comfy.QueuePrompt is executed only after a successful workflow load.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/platform/cloud/subscription/components/PricingTable.vue:0-0
Timestamp: 2026-05-07T11:23:16.180Z
Learning: In ComfyUI_frontend Vue components, do not use the Tailwind foreground alias utility classes like `text-success-foreground` (they are intentionally descoped/unknown to Tailwind in App Mode). Instead, use arbitrary-value CSS tokens such as `text-(--success-foreground)` (or an explicit semantic token like `text-(--success-background)` when the context calls for background vs foreground). During review, flag suggestions that use `text-*-foreground` aliases and prefer `text-(--<token>)` forms.

Comment on lines +11 to +18
const TOP_BELOW_CHROME =
'top-[calc(var(--spacing-layout-outer)+var(--spacing-layout-cell)+var(--spacing-layout-gutter))]'
const BOTTOM_ABOVE_CHROME =
'bottom-[calc(var(--spacing-layout-outer)+var(--spacing-layout-cell)+var(--spacing-layout-gutter))]'
const RIGHT_EDGE = 'right-(--spacing-layout-outer)'
const LEFT_EDGE = 'left-(--spacing-layout-outer)'
const SLOT_MAX_H =
'max-h-[calc(100%-var(--spacing-layout-outer)*2-var(--spacing-layout-cell)*2-var(--spacing-layout-gutter)*2)]'
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/appMode/layout/panels/panelPresetClasses.ts | head -30

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1590


🌐 Web query:

Tailwind CSS arbitrary values spaces underscores calc function

💡 Result:

In Tailwind CSS, arbitrary values use square bracket notation like w-[200px] to apply any CSS value directly.[1] Spaces are not allowed in class names because browsers split classes on spaces, so they must be replaced with underscores () which Tailwind automatically converts to spaces at build time.[1][2] For example, grid-cols-[1fr_500px_2fr] becomes grid-cols: 1fr 500px 2fr in CSS.[1] This applies specifically to the calc function in arbitrary values. Spaces inside calc must be replaced with underscores: w-[calc(100%-2rem)] generates width: calc(100% - 2rem);[3][1][4][5] Spaces like w-[calc(100% - 2rem)] break because the browser parses it as multiple classes.[6][7] Underscores are converted to spaces unless in contexts like URLs (where they are preserved), the first argument of var or theme, or explicitly escaped with backslash (\) to keep the underscore.[1][8][2] For calc, use underscores for operator spacing: h-[calc(100vh_-64px)].[3] This behavior is unchanged in Tailwind CSS v4 (latest release v4.2.4 as of 2026-04).[9] Examples: - Valid: w-[calc(100%-20px)], p-[calc(3rem*_2)] - Invalid (use ): w-[calc(100% - 20px)] In JS/JSX, escape backslashes with String.rawhello\\_world to preserve \ if needed.[8]

Citations:


🌐 Web query:

CSS calc() operator whitespace requirements specification

💡 Result:

The CSS calc function requires whitespace on both sides of the + (addition) and - (subtraction) operators due to the formal grammar defined in the CSS Values and Units specification. This is explicitly stated in section 8.1.1 (Syntax) of CSS Values and Units Module Level 3 (and carried forward in Levels 4 and 5): "In addition, white space is required on both sides of the + and - operators. (The * and / operators can be used without white space around them.)" [1][2]. The grammar productions are: = calc( ) = [ [ '+' | '-' ] ]* = [ [ '' | '/' ] ] = | | | ( ) [1][2][3]. This requirement prevents parsing ambiguities. Without spaces, expressions like calc(50% -8px) are parsed as a percentage followed by a negative length (-8px), which is invalid, rather than subtraction [4][5][6][3]. Similarly, calc(8px + -50%) would be a length followed by a unary minus on the percentage [4][6]. The hyphen (-) can appear in CSS identifiers/dimensions, so spaces distinguish the binary operator [7][6]. In contrast, * (multiplication) and / (division) do not require whitespace, though it is recommended for consistency [4][1][2][3]. This rule remains unchanged in the latest specifications (Levels 4 and 5 as of 2024/2025 drafts) [8][9]. MDN confirms: "The + and - operators must be surrounded by whitespace" [4][10][3].

Citations:


calc() expressions are missing required whitespace around + and - operators.

When an arbitrary value needs to contain a space, use an underscore (_) instead and Tailwind will automatically convert it to a space at build-time. The CSS spec (CSS Values and Units Module) explicitly requires whitespace on both sides of + and - in calc() — omitting it produces spec-invalid CSS that creates parsing ambiguity. The correct pattern is w-[calc(100%_-_2rem)] for proper parsing.

SLOT_MAX_H is especially at risk: calc(100%-var(...)…) can be misread as 100% followed by a unary-negated custom property, which is syntactically invalid.

🛠️ Proposed fix — use `_` as space placeholder
-const TOP_BELOW_CHROME =
-  'top-[calc(var(--spacing-layout-outer)+var(--spacing-layout-cell)+var(--spacing-layout-gutter))]'
-const BOTTOM_ABOVE_CHROME =
-  'bottom-[calc(var(--spacing-layout-outer)+var(--spacing-layout-cell)+var(--spacing-layout-gutter))]'
+const TOP_BELOW_CHROME =
+  'top-[calc(var(--spacing-layout-outer)_+_var(--spacing-layout-cell)_+_var(--spacing-layout-gutter))]'
+const BOTTOM_ABOVE_CHROME =
+  'bottom-[calc(var(--spacing-layout-outer)_+_var(--spacing-layout-cell)_+_var(--spacing-layout-gutter))]'
-const SLOT_MAX_H =
-  'max-h-[calc(100%-var(--spacing-layout-outer)*2-var(--spacing-layout-cell)*2-var(--spacing-layout-gutter)*2)]'
+const SLOT_MAX_H =
+  'max-h-[calc(100%_-_var(--spacing-layout-outer)*2_-_var(--spacing-layout-cell)*2_-_var(--spacing-layout-gutter)*2)]'
🤖 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 `@src/components/appMode/layout/panels/panelPresetClasses.ts` around lines 11 -
18, The Tailwind arbitrary calc() values in TOP_BELOW_CHROME,
BOTTOM_ABOVE_CHROME and SLOT_MAX_H are missing required whitespace around + and
-; update those string constants to use underscores (_) as space placeholders
inside the calc expressions (e.g. use _+_ and _-_ between terms) so Tailwind
emits valid CSS, and also fix RIGHT_EDGE and LEFT_EDGE to use the correct
arbitrary var syntax (use a bracketed arbitrary value for
var(--spacing-layout-outer) instead of the current parenthesis form); modify the
constants TOP_BELOW_CHROME, BOTTOM_ABOVE_CHROME, SLOT_MAX_H, RIGHT_EDGE and
LEFT_EDGE accordingly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — out of scope. The hex/rgb values in panelPresetClasses are deliberate visual identity (preset accents), not theme tokens. Documented in the LatentPreview comment block as the project's pattern for fixed-identity placeholder visuals.

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.

The PR description images show:

  • Image 1: App workspace with two floating result cards, a right-side prompt panel, and a centered “Output” progress window (step 13/30, 32s). Filename cards have actions; the Run controls are top-right.
  • Image 2: A masonry gallery of cyberpunk-eye results with the same right-side prompt/size panel; “Number of runs” and Run in header.
  • Image 3: Bunny calligraphy set with an in-progress overlay bar (step 23/30) and a large detail card.
  • Image 4: Tall 512×768 gallery with a small floating progress dialog; right panel shows a long prompt.
  • Image 5: 1024×1024 variation grid with a large selected image on the left and an Input panel on the right.

@eliheuer Thanks — acknowledged on the fixed-identity color accents; I won’t push theme tokens there.

Separate from colors, my note on src/components/appMode/layout/panels/panelPresetClasses.ts (Line 11–18) was about calc() spacing. To avoid calc parsing issues and keep Tailwind arbitrary values valid, we should add underscores as space placeholders around + and - inside the calc() expressions. RIGHT_EDGE/LEFT_EDGE using the v4 var shorthand (right-(--spacing-layout-outer)/left-(--…)) are fine as-is.

Here’s the minimal, targeted tweak:

// panelPresetClasses.ts
const TOP_BELOW_CHROME =
  'top-[calc(var(--spacing-layout-outer)_+_var(--spacing-layout-cell)_+_var(--spacing-layout-gutter))]'

const BOTTOM_ABOVE_CHROME =
  'bottom-[calc(var(--spacing-layout-outer)_+_var(--spacing-layout-cell)_+_var(--spacing-layout-gutter))]'

const RIGHT_EDGE = 'right-(--spacing-layout-outer)'
const LEFT_EDGE = 'left-(--spacing-layout-outer)'

const SLOT_MAX_H =
  'max-h-[calc(100%_-_var(--spacing-layout-outer)*2_-_var(--spacing-layout-cell)*2_-_var(--spacing-layout-gutter)*2)]'

If you’re okay with this tiny calc fix, I’ll consider the thread closed with colors deferred.


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/panelPresetClasses.ts:11-18
Timestamp: 2026-05-08T23:32:44.886Z
Learning: ComfyUI_frontend: In panelPresetClasses.ts and similar UI “preset accent” contexts, hard-coded hex/rgb values are an intentional visual identity and are not to be converted to theme tokens. Treat such colors as out-of-scope for theming refactors unless the author requests otherwise.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.

Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 11531
File: src/composables/maskeditor/brushDrawingUtils.ts:14-14
Timestamp: 2026-04-22T04:06:01.353Z
Learning: In the Comfy-Org/ComfyUI_frontend codebase, `knip` is used to detect unused exports. When reviewing TypeScript/TSX code, avoid recommending or introducing exported types/functions/constants that have no concrete external consumers (e.g., they’re only used within the same module). Only recommend exporting when there is an actual external usage outside the module. If something might be needed externally later, keep it non-exported for now and defer exporting until an external consumer is added.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/useAppPanelLayout.ts:0-0
Timestamp: 2026-05-07T11:21:56.210Z
Learning: In the Vue 3/TypeScript frontend (ComfyUI_frontend), when LiteGraph mutates/reuses graph node arrays in place, Vue reactivity may not trigger if you only mirror/mutate the existing array. When updating state backed by a shallowRef (e.g., for derived panel/layout data), assign a new array reference when mirroring nodes—e.g., `graphNodes.value = [...(app.rootGraph?.nodes ?? [])]`—so recomputed values like `inputEntries`/`panelRows` rerun on reconfigure.

Comment on lines 59 to 77
const selectInputsStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:inputs',
title: t('builderToolbar.inputs'),
subtitle: t('builderToolbar.inputsDescription'),
icon: 'icon-[lucide--mouse-pointer-click]'
subtitle: t('builderToolbar.inputsDescription')
}

const selectOutputsStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:outputs',
title: t('builderToolbar.outputs'),
subtitle: t('builderToolbar.outputsDescription'),
icon: 'icon-[lucide--mouse-pointer-click]'
subtitle: t('builderToolbar.outputsDescription')
}

const arrangeStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:arrange',
title: t('builderToolbar.arrange'),
subtitle: t('builderToolbar.arrangeDescription'),
icon: 'icon-[lucide--layout-panel-left]'
subtitle: t('builderToolbar.arrangeDescription')
}

const steps = [selectInputsStep, selectOutputsStep, arrangeStep]
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether runtime locale switching is wired in this app.
rg -nP -C2 'locale\.value\s*=|i18n\.global\.locale|setLocale' --type=ts --type=vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Search for locale switching patterns without type restriction
rg -n 'locale\s*[:=]|setLocale|switchLocale|i18n\.global\.locale' --type-add 'web:*.{js,ts,vue,jsx,tsx}' -t web -C2 | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 7105


🏁 Script executed:

#!/bin/bash
# Look for i18n configuration and setup
fd -e ts -e js | xargs grep -l "createI18n\|useI18n" | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1562


🏁 Script executed:

#!/bin/bash
# Check how BuilderToolbar.vue uses the steps variable
cat -n src/components/builder/BuilderToolbar.vue | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3306


Step title and subtitle strings won't reactively update on locale change.

Runtime locale switching is supported in this app (i18n.global.locale.value is set in GraphView.vue). The step objects at lines 59–77 call t() once at setup time, storing static strings. When the user switches locale, these cached values remain unchanged. Wrap steps with computed() to ensure titles and subtitles update reactively.

Suggested fix
-const selectInputsStep: BuilderToolbarStep<BuilderStepId> = {
-  id: 'builder:inputs',
-  title: t('builderToolbar.inputs'),
-  subtitle: t('builderToolbar.inputsDescription')
-}
-
-const selectOutputsStep: BuilderToolbarStep<BuilderStepId> = {
-  id: 'builder:outputs',
-  title: t('builderToolbar.outputs'),
-  subtitle: t('builderToolbar.outputsDescription')
-}
-
-const arrangeStep: BuilderToolbarStep<BuilderStepId> = {
-  id: 'builder:arrange',
-  title: t('builderToolbar.arrange'),
-  subtitle: t('builderToolbar.arrangeDescription')
-}
-
-const steps = [selectInputsStep, selectOutputsStep, arrangeStep]
+const steps = computed<BuilderToolbarStep<BuilderStepId>[]>(() => [
+  {
+    id: 'builder:inputs',
+    title: t('builderToolbar.inputs'),
+    subtitle: t('builderToolbar.inputsDescription')
+  },
+  {
+    id: 'builder:outputs',
+    title: t('builderToolbar.outputs'),
+    subtitle: t('builderToolbar.outputsDescription')
+  },
+  {
+    id: 'builder:arrange',
+    title: t('builderToolbar.arrange'),
+    subtitle: t('builderToolbar.arrangeDescription')
+  }
+])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selectInputsStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:inputs',
title: t('builderToolbar.inputs'),
subtitle: t('builderToolbar.inputsDescription'),
icon: 'icon-[lucide--mouse-pointer-click]'
subtitle: t('builderToolbar.inputsDescription')
}
const selectOutputsStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:outputs',
title: t('builderToolbar.outputs'),
subtitle: t('builderToolbar.outputsDescription'),
icon: 'icon-[lucide--mouse-pointer-click]'
subtitle: t('builderToolbar.outputsDescription')
}
const arrangeStep: BuilderToolbarStep<BuilderStepId> = {
id: 'builder:arrange',
title: t('builderToolbar.arrange'),
subtitle: t('builderToolbar.arrangeDescription'),
icon: 'icon-[lucide--layout-panel-left]'
subtitle: t('builderToolbar.arrangeDescription')
}
const steps = [selectInputsStep, selectOutputsStep, arrangeStep]
const steps = computed<BuilderToolbarStep<BuilderStepId>[]>(() => [
{
id: 'builder:inputs',
title: t('builderToolbar.inputs'),
subtitle: t('builderToolbar.inputsDescription')
},
{
id: 'builder:outputs',
title: t('builderToolbar.outputs'),
subtitle: t('builderToolbar.outputsDescription')
},
{
id: 'builder:arrange',
title: t('builderToolbar.arrange'),
subtitle: t('builderToolbar.arrangeDescription')
}
])
🤖 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 `@src/components/builder/BuilderToolbar.vue` around lines 59 - 77, The step
title/subtitle strings (selectInputsStep, selectOutputsStep, arrangeStep) call
t() once at setup and become static so they won't update on locale change;
instead wrap the steps array in a computed() that returns the three step objects
(calling t() inside the computed) so titles/subtitles recompute when
i18n.global.locale.value changes—update the definition of steps to be a computed
property that builds objects using t() rather than storing the t() results at
setup time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — runtime locale switching isn't wired anywhere in this app today (no users switch locale mid-session), so the cached translation is fine. If/when locale switching lands, this site will join the broader audit pass.

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.

The PR description images show:

  • Image 1 (App workspace with floating image previews and live generation progress): A grid-canvas UI with two image cards titled “Anima_00033_.png” and “Anima_00034_.png”, a right parameters panel, and a floating “Output” dialog showing “step 13/30 — 32s” with a red X.
  • Image 2 (Gallery of “green-eyes-001”): A masonry grid of six cyberpunk-eye images with filenames; right panel shows prompt and 512×512 size fields; top-right controls include “Number of runs — 1” and “Run”.
  • Image 3 (“anima-bunny-001”): Grid of bunny calligraphy images; an in‑progress overlay reads “step 23/30 — 14s”; a large detail card on the right.
  • Image 4 (Tall gallery 512×768): Many eye variants; small floating “Output” dialog “step 26/30 — 32s”; right panel “image_anima_preview--008” with 512×768 size.
  • Image 5 (“bunny-anima-001” 1024×1024): Large selected image left, variations center, and an “Input” panel right with prompt and 1024×1024 size.

@eliheuer Thanks for the clarification. Acknowledged—since runtime locale switching isn’t wired today, leaving the BuilderToolbar step titles/subtitles as setup-time t() values is fine. I won’t push this change in this PR.

If you’d like, I can open a follow‑up issue to track “Make builder toolbar i18n reactive when locale switching lands.” Want me to file that on GitHub with a backlink to this comment?

(꒦໊ྀʚ兔)


✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/builder/BuilderToolbar.vue:59-77
Timestamp: 2026-05-08T23:31:37.825Z
Learning: Repo Comfy-Org/ComfyUI_frontend (PR `#11317`): Runtime locale switching is not wired; users do not switch locale mid-session. It is acceptable for Vue components to call t() in setup and cache strings (e.g., step titles in src/components/builder/BuilderToolbar.vue). Defer reactive i18n changes until locale switching is introduced.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: henrikvilhelmberglund
Repo: Comfy-Org/ComfyUI_frontend PR: 7617
File: src/components/actionbar/ComfyActionbar.vue:301-308
Timestamp: 2025-12-18T16:03:02.066Z
Learning: In the ComfyUI frontend queue system, useQueuePendingTaskCountStore().count indicates the number of tasks in the queue, where count = 1 means a single active/running task and count > 1 means there are pending tasks in addition to the active task. Therefore, in src/components/actionbar/ComfyActionbar.vue, enable the 'Clear Pending Tasks' button only when count > 1 to avoid clearing the currently running task. The active task should be canceled using the 'Cancel current run' button instead. This rule should be enforced via a conditional check on the queue count, with appropriate disabled/aria-disabled states for accessibility, and tests should verify behavior for count = 1 and count > 1.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:2-89
Timestamp: 2026-02-07T14:46:21.179Z
Learning: In Vue components (e.g., TreeExplorerV2.vue and similar Recka UI components) avoid wrapping ContextMenuRoot with a dynamic <component :is="..."> and rendering alternative branches. Use v-if / v-else to render separate ContextMenuRoot and non-ContextMenuRoot branches to ensure correct behavior with Reka UI, even if it duplicates some tree structure. This guideline applies to Vue components under src/components and can be checked by ensuring there is a dedicated render path for the ContextMenuRoot without dynamic component switching.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2.vue:3-5
Timestamp: 2026-02-08T16:04:58.463Z
Learning: In TreeExplorerV2.vue and similar Re- ka UI Tree components, avoid defensive copies in bindings like :expanded="[...expandedKeys]". Use v-model:expanded="expandedKeys" instead. TreeRoot follows Vue's controlled component pattern and emits update:expanded with the new array; it does not mutate the passed array. Defensive copies break reactivity and can desynchronize state. Ensure components rely on update:expanded and do not mutate input arrays.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 8761
File: src/components/dialog/content/MissingModelsWarning.vue:21-27
Timestamp: 2026-02-10T00:58:07.904Z
Learning: In the ComfyUI_frontend Vue codebase, replace raw <button> HTML elements with the shared Button component located at src/components/ui/button/Button.vue. Import and use it with appropriate variants (e.g., variant="link") to align with the design system. Apply this pattern across Vue components under src/components, ensuring consistent styling and behavior instead of ad-hoc button markup.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 9511
File: src/components/common/WorkflowActionsDropdown.vue:53-60
Timestamp: 2026-03-06T22:05:25.071Z
Learning: When using PrimeVue's v-tooltip in Vue components, be aware of a known bug where the tooltip centers on the trigger. If positioning requires a left-anchored placement, it may be acceptable and necessary to apply a deliberate workaround (e.g., adjust root transform with translateX(calc(50% - 16px)) and reposition the tooltip arrow with left utilities) rather than flagging as fragile or using !important. This pattern may appear in multiple Vue components under src/components/ that use v-tooltip; apply the same left-anchoring adjustment where appropriate and document the rationale so future maintainers understand the intentional workaround.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10135
File: src/components/rightSidePanel/errors/TabErrors.vue:288-295
Timestamp: 2026-03-17T07:02:53.709Z
Learning: In TabErrors.vue, ensure the guard used for the singleRuntimeErrorGroup relies on some((e) => e.isRuntimeError) to detect a runtime error among the runtime/validation error cards. Do not switch to every() as that would be overly restrictive given the backend’s mutual exclusion: a run either yields a runtime error or a set of validation errors. This guidance applies to Vue components in the src/components directory (generalized to all .vue files under src/components).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/FeedbackCell.vue:0-0
Timestamp: 2026-04-28T07:02:16.613Z
Learning: In Vue SFCs (files under src/**/*.vue), avoid using Tailwind “important” modifiers (`!`, e.g., `!bg-*`, `!text-*`). For overrides that are truly needed, prefer defining/overriding styles in the component’s scoped <style> block using Vue’s `:deep()` selectors instead of relying on Tailwind `!` (e.g., follow the approach used in FeedbackCell.vue and ModeToggleCell.vue).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/IconCell.vue:8-37
Timestamp: 2026-04-28T07:02:32.365Z
Learning: For Vue components under src/components/**, prefer emitting events (e.g., this.$emit('activate')) to notify parents about state changes/interactions instead of using callback props (e.g., an onActivate prop). In parent components, bind listeners with the event syntax (activate), not with props-style binding (e.g., :on-activate).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/platform/cloud/subscription/components/PricingTable.vue:0-0
Timestamp: 2026-05-07T11:23:16.180Z
Learning: In ComfyUI_frontend Vue components, do not use the Tailwind foreground alias utility classes like `text-success-foreground` (they are intentionally descoped/unknown to Tailwind in App Mode). Instead, use arbitrary-value CSS tokens such as `text-(--success-foreground)` (or an explicit semantic token like `text-(--success-background)` when the context calls for background vs foreground). During review, flag suggestions that use `text-*-foreground` aliases and prefer `text-(--<token>)` forms.

Comment thread src/components/graph/GraphCanvas.vue Outdated
Comment on lines +141 to +165
const panelOnLeft = !!insets.panelRect && insets.panelRect.x === 0
const featureOnRight = panelOnLeft

const placements = templatePlacements(
N,
avail.cols,
avail.rows,
aspectArr,
featureOnRight
)

// Pixel boundaries to snap edge-touching tiles to. The cell grid
// floors the panel's column position, so when the panel doesn't sit
// on a cell boundary there's a sub-cell sliver between the last
// avail cell and the panel/canvas edge. Tiles in the last avail
// column or row stretch to consume that sliver, leaving exactly
// one standard gutter to the chrome.
const rightBoundaryWithPanel =
insets.panelRect && insets.panelRect.x > 0
? insets.panelRect.x - CHROME_GUTTER
: viewW - CHROME_OUTER
const leftBoundaryWithPanel =
insets.panelRect && insets.panelRect.x === 0
? insets.panelRect.x + insets.panelRect.w + CHROME_GUTTER
: CHROME_OUTER
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the chrome-rail inset when detecting left-side panels.

computeAvailRect() already normalizes panel coordinates with pr.x - CHROME_OUTER, so a left-docked/left-floating panel anchored to the chrome rail is effectively “left-side” at x === CHROME_OUTER, not 0. The x === 0 checks here never flip featureOnRight in that case, and they also skip the left-edge sliver correction.

💡 Suggested fix
-  const panelOnLeft = !!insets.panelRect && insets.panelRect.x === 0
+  const panelOnLeft =
+    !!insets.panelRect && insets.panelRect.x <= CHROME_OUTER
   const featureOnRight = panelOnLeft
@@
   const leftBoundaryWithPanel =
-    insets.panelRect && insets.panelRect.x === 0
+    insets.panelRect && insets.panelRect.x <= CHROME_OUTER
       ? insets.panelRect.x + insets.panelRect.w + CHROME_GUTTER
       : CHROME_OUTER
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const panelOnLeft = !!insets.panelRect && insets.panelRect.x === 0
const featureOnRight = panelOnLeft
const placements = templatePlacements(
N,
avail.cols,
avail.rows,
aspectArr,
featureOnRight
)
// Pixel boundaries to snap edge-touching tiles to. The cell grid
// floors the panel's column position, so when the panel doesn't sit
// on a cell boundary there's a sub-cell sliver between the last
// avail cell and the panel/canvas edge. Tiles in the last avail
// column or row stretch to consume that sliver, leaving exactly
// one standard gutter to the chrome.
const rightBoundaryWithPanel =
insets.panelRect && insets.panelRect.x > 0
? insets.panelRect.x - CHROME_GUTTER
: viewW - CHROME_OUTER
const leftBoundaryWithPanel =
insets.panelRect && insets.panelRect.x === 0
? insets.panelRect.x + insets.panelRect.w + CHROME_GUTTER
: CHROME_OUTER
const panelOnLeft =
!!insets.panelRect && insets.panelRect.x <= CHROME_OUTER
const featureOnRight = panelOnLeft
const placements = templatePlacements(
N,
avail.cols,
avail.rows,
aspectArr,
featureOnRight
)
// Pixel boundaries to snap edge-touching tiles to. The cell grid
// floors the panel's column position, so when the panel doesn't sit
// on a cell boundary there's a sub-cell sliver between the last
// avail cell and the panel/canvas edge. Tiles in the last avail
// column or row stretch to consume that sliver, leaving exactly
// one standard gutter to the chrome.
const rightBoundaryWithPanel =
insets.panelRect && insets.panelRect.x > 0
? insets.panelRect.x - CHROME_GUTTER
: viewW - CHROME_OUTER
const leftBoundaryWithPanel =
insets.panelRect && insets.panelRect.x <= CHROME_OUTER
? insets.panelRect.x + insets.panelRect.w + CHROME_GUTTER
: CHROME_OUTER
🤖 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 `@src/renderer/extensions/linearMode/outputWindowStore.ts` around lines 141 -
165, The code incorrectly treats a left-docked panel as x === 0; update the
panel-side detection and boundary logic to account for the chrome-rail offset
used by computeAvailRect (compare against CHROME_OUTER). Concretely, set
panelOnLeft using insets.panelRect.x === CHROME_OUTER (instead of === 0), set
featureOnRight from panelOnLeft as before, and update the
rightBoundaryWithPanel/leftBoundaryWithPanel conditions to test panelRect.x >
CHROME_OUTER and panelRect.x === CHROME_OUTER respectively so the left-edge
sliver correction and feature placement behave correctly (references:
insets.panelRect, panelOnLeft, featureOnRight, rightBoundaryWithPanel,
leftBoundaryWithPanel, computeAvailRect).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — the left-side detection works for the current chrome layout (panel either docked at x=0 or floating). The chrome-rail-inset edge case the comment describes is real but extremely niche; will fix-forward if the dashboard layout reports incorrect placements in practice.

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.

The PR description images show:

  • Image 1: A node-canvas workspace with multiple floating output cards and a right-side parameters panel; a small “Output” window shows live progress “step 13/30 — 32s”.
  • Image 2–4: Masonry-style galleries of eye-themed images with a right parameters panel; progress overlays appear on inflight tiles.
  • Image 5: A large 1024×1024 bunny image with smaller variations and an “Input” panel on the right.

@eliheuer Thanks for the context. SGTM to defer—given the current chrome, treating left dock as x=0 is correct and this isn’t blocking.

Optional, future-proofing-only tweak (no behavior change today) that centralizes the “left side” check so switching to a rail-inset later is a one-liner change:

// Lines 141–165 area
const isLeftSide = (pr?: { x: number }) => !!pr && pr.x <= CHROME_OUTER

const panelOnLeft = isLeftSide(insets.panelRect)
const featureOnRight = panelOnLeft

// ...
const leftBoundaryWithPanel =
  insets.panelRect && isLeftSide(insets.panelRect)
    ? insets.panelRect.x + insets.panelRect.w + CHROME_GUTTER
    : CHROME_OUTER

If you’d like, I can open a tiny follow-up issue to track “rail inset awareness for dashboardSlots()” and add a unit test that asserts correct feature/edge behavior for:

  • left dock at x=0 (today)
  • hypothetical rail-inset at x=CHROME_OUTER (future)

Your call—happy to proceed either way.

(_/)
(•_•) — noted; curious to see if a rail inset ever lands
/︶\


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.

Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 11531
File: src/composables/maskeditor/brushDrawingUtils.ts:14-14
Timestamp: 2026-04-22T04:06:01.353Z
Learning: In the Comfy-Org/ComfyUI_frontend codebase, `knip` is used to detect unused exports. When reviewing TypeScript/TSX code, avoid recommending or introducing exported types/functions/constants that have no concrete external consumers (e.g., they’re only used within the same module). Only recommend exporting when there is an actual external usage outside the module. If something might be needed externally later, keep it non-exported for now and defer exporting until an external consumer is added.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/renderer/extensions/linearMode/useOutputWindowSync.ts:0-0
Timestamp: 2026-04-28T07:03:44.560Z
Learning: When calling outputWindowStore.upsert() updates window entries in place (e.g., via Object.assign), any Vue watchers/computed reactions that depend on store.windows must be configured with { deep: true } (e.g., in src/renderer/extensions/linearMode/useOutputWindowSync.ts) so they react to in-place mutations rather than only reference changes.

Comment thread src/renderer/extensions/linearMode/SelectionChrome.vue
Comment thread src/views/LinearView.vue
eliheuer added 2 commits May 8, 2026 16:28
BuilderBackdrop drag re-entrance
- handlePointerDown bails early when dragStart !== null. Prevents a
  multi-touch / second-mouse-button pointerdown from clobbering the
  active drag's pointer-capture state.

SelectionChrome keyboard a11y
- The selection-toggle root div was pointer-only. Add role="button",
  tabindex="0", aria-pressed, plus @keydown.enter and @keydown.space
  handlers so keyboard users can toggle selection.

GraphCanvas SideToolbar gate
- LiteGraphCanvasSplitterOverlay's sidebarPanelVisible is gated on
  !isBuilderMode. The SideToolbar buttons appeared in builder mode
  but clicking them did nothing because the panel they'd open is
  hidden. Suppress the buttons in builder mode too.

BuilderPanel + LinearView sidebar location
- Both branches hardcoded the sidebar gutter on the left. Read
  Comfy.Sidebar.Location and apply the gutter on whichever side the
  user has it pinned. In LinearView's showLayoutView splitter, also
  swap panel order (LayoutView/Extension) and move SideToolbar to
  the matching side.
CONTRIBUTING.md asks contributors to (a) document significant
architectural decisions via ADRs, and (b) ensure `pnpm format` is
clean before submitting. Both checked here.

ADR 0009 records the rationale for unifying App Mode and App Builder
behind a single store (`appModeStore`) and shared chrome
(`AppChrome`, `FloatingPanel`), the alternatives considered (a
translation layer, save-time generation), and the consequences —
including the moodboard semantics shift in the output workspace
and the larger-than-typical PR shape.

Format: `oxfmt` collapsed the multi-line shadow declaration in
`src/assets/css/style.css:9-10` to a single line. Cosmetic only.
@eliheuer
Copy link
Copy Markdown
Author

eliheuer commented May 9, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/extensions/linearMode/MediaOutputPreview.vue (1)

42-53: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Forward fit behavior to the video branch as well.

Line 16 introduces fit, but in Line 42 onward the video path still hardcodes object-contain and does not pass fit. fit='cover' therefore won’t affect video previews.

🔧 Suggested fix
     <VideoPreview
       v-else
       :src="output.url"
       :label="outputLabel"
       :hide-info="hideInfo"
+      :fit="fit"
       :class="
         cn(
-          'flex-1 object-contain md:p-3 md:contain-size',
+          'flex-1 md:p-3 md:contain-size',
           attrs.class as string
         )
       "
     />
🤖 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 `@src/renderer/extensions/linearMode/MediaOutputPreview.vue` around lines 42 -
53, The video preview branch ignores the fit prop and always uses
object-contain, so forward the fit behavior to VideoPreview: update the
VideoPreview usage (component name VideoPreview) to accept the fit prop (pass
:fit="fit") and derive its class via cn like the image branch so that when fit
=== 'cover' it applies object-cover (or object-contain otherwise) while still
merging attrs.class; adjust the VideoPreview props usage to mirror the image
branch's handling of fit and cn so fit='cover' affects video previews.
src/renderer/extensions/linearMode/LinearWelcome.test.ts (1)

65-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset panelPreset in beforeEach to avoid cross-test leakage.

panelPreset is now hoisted module state, so leaving it unreset can make later tests order-dependent if they mutate it.

💡 Suggested patch
 beforeEach(() => {
   hasNodes.value = false
   hasOutputs.value = false
+  panelPreset.value = 'right-dock'
   vi.clearAllMocks()
 })
🤖 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 `@src/renderer/extensions/linearMode/LinearWelcome.test.ts` around lines 65 -
69, The beforeEach block must also reset the hoisted module state panelPreset to
avoid cross-test leakage; update the beforeEach that currently sets
hasNodes.value, hasOutputs.value and calls vi.clearAllMocks() to also set
panelPreset (e.g., panelPreset.value = null or its original default) so each
test starts with a clean panelPreset state.
♻️ Duplicate comments (4)
src/components/appMode/layout/PanelHeader.vue (1)

16-25: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Make expandLabel and collapseLabel part of the required prop contract.

The chevron control is still icon-only, but these props remain optional, so aria-label can become undefined even though the comment says they are required. Tightening the prop type here prevents the accessible-name gap from slipping back in at the next call site.

♿ Proposed fix
-  /** Required when `collapsible` is true so icon-only buttons have accessible names. */
-  expandLabel?: string
-  collapseLabel?: string
+  /** Required so the icon-only collapse control always has an accessible name. */
+  expandLabel: string
+  collapseLabel: string

Based on learnings, "If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action."

Also applies to: 61-62

🤖 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 `@src/components/appMode/layout/PanelHeader.vue` around lines 16 - 25, The prop
types in the defineProps block for PanelHeader.vue declare expandLabel and
collapseLabel as optional which allows aria-label to be undefined on the
icon-only chevron button; make expandLabel and collapseLabel required by
removing the optional markers so the prop contract enforces callers to supply
accessible names (update the defineProps generic to require expandLabel: string
and collapseLabel: string and adjust any related JSDoc comment if present), then
run typecheck to find and fix any call sites that must now pass those props.
src/assets/css/style.css (1)

75-78: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore a keyboard-only focus indicator for themed widgets.

These rules remove the native outline/ring from the themed inputs, selects, and pill wrappers, but the provided file no longer includes the :focus-visible replacement that previously restored keyboard focus. On the dark canvas, that makes focus hard to track again.

♿ Possible fix
 .app-mode-themed .p-select:focus-within,
 .app-mode-themed .p-select.p-focus {
   border-color: var(--fg-color);
 }
+
+.app-mode-themed textarea:focus-visible,
+.app-mode-themed input:focus-visible,
+.app-mode-themed .p-select:focus-visible,
+.app-mode-themed .p-select:has(:focus-visible),
+.app-mode-themed .bg-component-node-widget-background:has(:focus-visible) {
+  outline: 2px solid rgb(255 255 255 / 0.7);
+  outline-offset: 1px;
+}

Also applies to: 114-127

🤖 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 `@src/assets/css/style.css` around lines 75 - 78, The themed CSS removed the
keyboard-only focus ring — add a :focus-visible rule to restore an accessible
focus indicator for themed widgets: add selectors alongside .app-mode-themed
.p-select:focus-within and .app-mode-themed .p-select.p-focus that target the
keyboard focus state (e.g., .app-mode-themed .p-select:focus-visible and the
equivalent for .p-input and .p-pill wrappers) and apply a visible
outline/box-shadow or border-color using the existing color variables (e.g.,
var(--fg-color) or a dedicated --focus-color/--focus-ring) so keyboard users see
a distinct focus ring on the dark canvas.
src/renderer/extensions/linearMode/LinearPreview.vue (1)

115-131: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block rerun/reuse when the source workflow can't be loaded.

loadWorkflow() silently returns on undefined / missing extracted workflows, but rerun() still calls runButtonClick(e). That can queue the currently loaded graph instead of the selected output's workflow. The reuse-parameters button has the same silent-failure path.

💡 Minimal fix
-async function loadWorkflow(item: AssetItem | undefined) {
-  if (!item) return
+async function loadWorkflow(item: AssetItem | undefined): Promise<boolean> {
+  if (!item) return false
   const { workflow } = await extractWorkflowFromAsset(item)
-  if (!workflow) return
+  if (!workflow) return false

-  if (workflow.id !== app.rootGraph.id) return app.loadGraphData(workflow)
+  if (workflow.id !== app.rootGraph.id) {
+    await app.loadGraphData(workflow)
+    return true
+  }
   const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker
-  if (!changeTracker) return app.loadGraphData(workflow)
+  if (!changeTracker) {
+    await app.loadGraphData(workflow)
+    return true
+  }
   changeTracker.redoQueue = []
   await changeTracker.updateState([workflow], changeTracker.undoQueue)
+  return true
 }

 async function rerun(e: Event) {
   if (!runButtonClick) return
-  await loadWorkflow(selectedItem.value)
+  if (!(await loadWorkflow(selectedItem.value))) return
   runButtonClick(e)
 }

Based on learnings: "actions that rerun from an output must first load the originating workflow ... callers should early-return when the promise resolves to false so the prompt queue action only executes after a successful workflow load."

Also applies to: 147-150

🤖 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 `@src/renderer/extensions/linearMode/LinearPreview.vue` around lines 115 - 131,
loadWorkflow currently returns silently on failure which allows rerun (and the
reuse-parameters handler) to proceed and queue the wrong graph; change
loadWorkflow to return a boolean success flag (true when the workflow was
actually loaded/applied, false on any early return or failure) and update
callers like rerun (and the reuse-parameters handler referenced around lines
147-150) to await loadWorkflow(...) and early-return if it resolves to false
before calling runButtonClick or queuing actions; reference functions:
loadWorkflow, rerun, and the reuse-parameters handler so the callers only
proceed when loadWorkflow succeeds.
src/components/appMode/layout/LayoutView.vue (1)

175-187: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only enter drag mode after pointer capture succeeds.

The catch {} here still falls through to dragging = true, so a rejected setPointerCapture() leaves the handler in the same half-started pan state this code path was trying to avoid.

Proposed fix
   if (!dragging) {
     const dx = e.clientX - dragStart.x
     const dy = e.clientY - dragStart.y
     if (dx * dx + dy * dy < DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) return
     try {
       bgRef.value?.setPointerCapture(dragStart.pointerId)
+      dragging = true
     } catch {
-      // Some browsers reject capture on non-primary pointers.
+      endDrag()
+      return
     }
-    dragging = true
   }

As per coding guidelines, "Implement proper error handling in all 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 `@src/components/appMode/layout/LayoutView.vue` around lines 175 - 187, The
pointermove handler currently sets dragging = true even if
bgRef.value.setPointerCapture(dragStart.pointerId) throws; modify the handler so
that dragging is only set to true after setPointerCapture succeeds: move the
assignment into the try block (or explicitly return/abort from the catch) and
ensure failures (caught exceptions) stop entering drag mode; reference the
pointermove listener created by useEventListener, the variables dragStart,
dragging, bgRef, setPointerCapture and DRAG_THRESHOLD_PX when locating and
updating the code.
🧹 Nitpick comments (3)
src/components/appMode/layout/cells/BatchCountCell.vue (1)

3-9: 💤 Low value

Prefer $t in template; remove the useI18n() import

t is never called in the script body — both usages are template expressions. Per the repo learning, when t is only needed in the template, use the built-in $t and drop the useI18n import entirely.

♻️ Proposed refactor
-import { useI18n } from 'vue-i18n'
 
 ...
 
-const { t } = useI18n()
-      {{ t('linearMode.runCount') }}
+      {{ $t('linearMode.runCount') }}
 ...
-      :aria-label="t('linearMode.runCount')"
+      :aria-label="$t('linearMode.runCount')"

Based on learnings: "In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script."

🤖 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 `@src/components/appMode/layout/cells/BatchCountCell.vue` around lines 3 - 9,
The file imports useI18n and destructures const { t } = useI18n() but t is never
used in script scope — it's only referenced in the template; remove the useI18n
import and the const { t } = useI18n() line from BatchCountCell.vue and update
template usages to call the global $t instead of t so the component relies on
the built-in template translator.
src/utils/widgetSubtitleUtil.test.ts (2)

48-66: 💤 Low value

Add a 'dimension' (singular) test case for the size-pattern regex.

The regex uses dimensions? — the ? makes the final s optional — but the test suite only covers the plural form 'dimensions'. A future regression that changes dimensions? to dimensions would not be caught by the existing array.

✅ Suggested addition
   it.each([
     'width',
     'height',
     'image_width',
     'image_height',
     'size',
-    'dimensions'
+    'dimensions',
+    'dimension'
   ])('overrides type with "size" for name %s', (name) => {
🤖 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 `@src/utils/widgetSubtitleUtil.test.ts` around lines 48 - 66, The size-pattern
test in widgetSubtitleUtil.test.ts only covers the plural 'dimensions' but the
regex uses 'dimensions?' so add the singular 'dimension' to the it.each array to
ensure widgetSubtitle({ name, spec: { type: 'INT', name } }) returns 'size' for
name 'dimension'; update the test case that currently lists
['width','height','image_width','image_height','size','dimensions'] to include
'dimension' and run the tests for the widgetSubtitle function.

1-19: ⚡ Quick win

The test correctly relies on i18n initialization. Since src/i18n.ts creates the i18n instance at module load with English translations eagerly imported, calling t('linearMode.widgetSubtitle.text') resolves to 'text' without explicit mocking. The Node.js environment handles this initialization without issue.

That said, adding an explicit mock would reduce implicit coupling and make the test's i18n dependency visible:

+import { vi, describe, expect, it } from 'vitest'
+
+vi.mock('@/i18n', () => ({
+  t: (key: string) => key.split('.').at(-1) ?? key
+}))
+
 import { widgetSubtitle } from '@/utils/widgetSubtitleUtil'

This keeps the test self-contained and decoupled from the global module initialization strategy.

🤖 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 `@src/utils/widgetSubtitleUtil.test.ts` around lines 1 - 19, Add an explicit
mock for the i18n module your util depends on instead of relying on global
module initialization: in the widgetSubtitle test file, mock the '@/i18n' export
(the t function) before using widgetSubtitle so
t('linearMode.widgetSubtitle.text') and friends return the expected strings;
update the test to call vi.mock('@/i18n', ...) (or the equivalent mocking
helper) to provide a deterministic t implementation for keys used by
widgetSubtitle, then run the existing assertions against widgetSubtitle({ name:
'unmapped', spec: { type: specType, name: '' } }) to keep the test
self-contained.
🤖 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 `@src/components/appMode/layout/AppChrome.vue`:
- Around line 217-226: The builder variant always reserves the sidebar gutter on
the left; change it to compute the real sidebar side from
useSettingStore().get('Comfy.Sidebar.Location') and conditionally apply the
gutter class to that side instead of always using 'right-0' and
left-(--sidebar-width...). In AppChrome.vue update the class expression that
builds the builder classes (the cn() call around the 'app-mode-themed app-chrome
...' string and the nested array for variant === 'builder') to read the sidebar
location once (e.g. const sidebarLocation =
useSettingStore().get('Comfy.Sidebar.Location')) and include either ['fixed
top-(--workflow-tabs-height) left-0 bottom-0', 'right-(--sidebar-width,0px)']
when sidebarLocation === 'right' or ['fixed top-(--workflow-tabs-height) right-0
bottom-0', 'left-(--sidebar-width,0px)'] when sidebarLocation === 'left', so the
gutter is reserved on the actual sidebar side.

In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 169-172: handlePointerDown currently arms dragging for any pointer
down inside layout-view which allows pointer events originating in floating
panels/chrome to start a workspace pan; update handlePointerDown to only set
dragStart when the pointer event target is inside the workspace layer (e.g.,
check event.target or use contains on the workspace DOM node/ref) and ignore
events originating from panel/chrome elements, and apply the same scoping change
to the other pointer-down handler(s) around the block that includes the
dragStart logic (the other occurrence referenced in the comment) so only
workspace-originating pointerdowns can seed a pan.
- Around line 119-150: dashboardInsetsKey currently only encodes panel x and
width so vertical moves/collapses don't change the key and relayoutDashboard
isn't retriggered; update dashboardInsetsKey (used in the watch that calls
windowStore.relayoutDashboard) to also include panel y and height (e.g. compute
rounded top/bottom or y/h using CHROME_STEP_PX like pStart/pEnd) so that changes
in vertical position or collapse/expand of dashboardInsets.value.panelRect cause
the key to change and relayoutDashboard(layoutW.value, layoutH.value,
dashboardInsets.value) runs when noZoomMode is true.

In `@src/components/appMode/layout/panels/useFlipReorder.ts`:
- Around line 33-52: The FLIP replay in onUpdated ignores users' reduced-motion
preference; add a guard using window.matchMedia('(prefers-reduced-motion:
reduce)').matches (or equivalent) and skip WAAPI animations when it returns
true. In practice, check this once at the start of the onUpdated callback (or
before the animation loop) and if reduced-motion is enabled simply return or
continue so the loop does not call el.animate; reference onUpdated, skipKey,
containerEl, prevRects and durationMs to find where to insert the check.

In `@src/components/builder/BuilderBackdrop.vue`:
- Around line 107-109: The backdrop currently always reserves the left gutter;
update BuilderBackdrop.vue so it computes the sidebar side from
Comfy.Sidebar.Location and emits a side-aware gutter class instead of the fixed
'left-(--sidebar-width,0px)'. Add a computed (e.g., sidebarLocation or
sidebarGutterClass) that reads Comfy.Sidebar.Location and returns either
'left-(--sidebar-width,0px)' or 'right-(--sidebar-width,0px)', then replace the
hard-coded string in the template class array with that computed value so the
fixed-position backdrop reserves the correct side and never overlaps the
sidebar.

In `@src/renderer/extensions/linearMode/AppInput.vue`:
- Around line 38-53: The current template switches between two render paths (the
wrapped <div> with ref="wrapper" and a bare <slot />) based on showSelection,
which remounts the slotted widget tree; instead, always render the slot and keep
a single wrapper element: move the <slot /> inside a persistent container (the
element that currently has ref="wrapper"), conditionally render or toggle the
SelectionChrome component (and any wrapper classes/styles) based on
showSelection (use v-if on <SelectionChrome> or class bindings) so the slot
subtree never unmounts; update references to wrapper, isPromoted, top, left,
width, height and the `@toggle`="togglePromotion" to match the single DOM node
approach.

In `@src/renderer/extensions/linearMode/DragReorderDemo.vue`:
- Around line 41-59: The looping demo animations (.drag-demo .shrinks,
.drag-demo .mover, .drag-demo .cursor using keyframes drag-shrink,
drag-translate, drag-cursor) should be opted-out for motion-sensitive users; add
a CSS `@media` (prefers-reduced-motion: reduce) rule that targets these selectors
and sets animation: none (or animation-duration: 0s / animation-iteration-count:
1) to stop the infinite animations and preserve layout without motion.

In `@src/renderer/extensions/linearMode/LinearArrange.vue`:
- Line 47: The inline style in LinearArrange.vue currently always shifts left
via translateX(calc(-0.5 * var(--sidebar-width, 0px))) which mis-centers when
the sidebar is on the left; change it to conditionally reserve the gutter on the
correct side by switching the sign based on sidebar position (e.g. use a
prop/computed like sidebarSide or a CSS class such as
.sidebar-left/.sidebar-right) and apply translateX(calc(sign * 0.5 *
var(--sidebar-width, 0px))) where sign is +1 for one side and -1 for the other
so the arrange preview shifts toward the center, not always left. Ensure the
condition is computed in the component (computed property or binding) and used
in the element with the current inline style expression.

In `@src/renderer/extensions/linearMode/LinearWelcome.vue`:
- Around line 44-53: The welcome panel currently hardcodes
left-(--spacing-layout-outer) which anchors it to the viewport left and causes
overlap with the global sidebar; update LinearWelcome.vue (the element with
data-testid="linear-welcome" and the cn(...) class construction) to compute the
sidebar side using Comfy.Sidebar.Location (or a new computed prop like
sidebarLocation) and then conditionally apply the gutter offset on the correct
side: when sidebarLocation === 'left' apply the left offset
(left-(--spacing-layout-outer)) and when 'right' apply the right offset
(right-(--spacing-layout-outer)) via the :class or :style binding so the docked
panel reserves the sidebar gutter on the proper side.

In `@src/renderer/extensions/linearMode/outputWindowStore.ts`:
- Around line 441-452: The collision check uses unsnapped candidate coordinates
but returns snapSpawn(c.x)/snapSpawn(c.y), which can cause overlaps after
snapping; for each candidate in the candidates loop (and the similar block
around the later candidates), first compute const sx = snapSpawn(c.x), sy =
snapSpawn(c.y), build newRect using { x: sx, y: sy, w: newW, h: newH } and run
rectsOverlap(newRect, entryRect(w)) against windows.value, then if not colliding
return { x: sx, y: sy }; apply the same change where the other candidate list is
checked so snapping is validated before the overlap test (symbols: candidates,
snapSpawn, rectsOverlap, entryRect, windows.value, placeFirstZoomTile).

In `@src/renderer/extensions/linearMode/SelectionChrome.vue`:
- Around line 17-31: The div acting as a button in SelectionChrome.vue lacks an
accessible name; add an explicit, localized accessible name (via aria-label or
aria-labelledby) to the element with role="button" (the group div using
`@pointerdown` and `@keydown` handlers) — e.g. bind an i18n/localized string (or
reference a visually-hidden span id) to aria-label/aria-labelledby on that div
so screen readers announce the control; ensure the label text is provided from
the existing localization system rather than hardcoding.

In `@src/stores/appModeStore.ts`:
- Around line 225-231: The current branch returns early when data?.layout is
falsy, which leaves previous workflow layout values (panelPreset,
panelCollapsed, panelWidthCells, panelRows) intact; change the logic so that
when layout is missing you explicitly reset those store values to their defaults
(e.g., null/false/0 or whatever the store's initial values are) instead of
returning; locate the block using the layout variable and the store fields
panelPreset, panelCollapsed, panelWidthCells, and panelRows and set them to the
intended default values when layout is undefined or null so stale layouts are
not carried into a new workflow.

In `@src/views/LinearView.vue`:
- Around line 124-129: The Splitter's persisted state-key is constant so when
sidebarOnLeft toggles the panel order the saved sizes get applied to the wrong
panel; update the Splitter usage to include sidebar side information in the
state-key (e.g., append or interpolate sidebarOnLeft into the state-key string)
so the key is unique per side, ensuring the Splitter component's state-key
reflects sidebarOnLeft and thus restores widths to the correct panel.

---

Outside diff comments:
In `@src/renderer/extensions/linearMode/LinearWelcome.test.ts`:
- Around line 65-69: The beforeEach block must also reset the hoisted module
state panelPreset to avoid cross-test leakage; update the beforeEach that
currently sets hasNodes.value, hasOutputs.value and calls vi.clearAllMocks() to
also set panelPreset (e.g., panelPreset.value = null or its original default) so
each test starts with a clean panelPreset state.

In `@src/renderer/extensions/linearMode/MediaOutputPreview.vue`:
- Around line 42-53: The video preview branch ignores the fit prop and always
uses object-contain, so forward the fit behavior to VideoPreview: update the
VideoPreview usage (component name VideoPreview) to accept the fit prop (pass
:fit="fit") and derive its class via cn like the image branch so that when fit
=== 'cover' it applies object-cover (or object-contain otherwise) while still
merging attrs.class; adjust the VideoPreview props usage to mirror the image
branch's handling of fit and cn so fit='cover' affects video previews.

---

Duplicate comments:
In `@src/assets/css/style.css`:
- Around line 75-78: The themed CSS removed the keyboard-only focus ring — add a
:focus-visible rule to restore an accessible focus indicator for themed widgets:
add selectors alongside .app-mode-themed .p-select:focus-within and
.app-mode-themed .p-select.p-focus that target the keyboard focus state (e.g.,
.app-mode-themed .p-select:focus-visible and the equivalent for .p-input and
.p-pill wrappers) and apply a visible outline/box-shadow or border-color using
the existing color variables (e.g., var(--fg-color) or a dedicated
--focus-color/--focus-ring) so keyboard users see a distinct focus ring on the
dark canvas.

In `@src/components/appMode/layout/LayoutView.vue`:
- Around line 175-187: The pointermove handler currently sets dragging = true
even if bgRef.value.setPointerCapture(dragStart.pointerId) throws; modify the
handler so that dragging is only set to true after setPointerCapture succeeds:
move the assignment into the try block (or explicitly return/abort from the
catch) and ensure failures (caught exceptions) stop entering drag mode;
reference the pointermove listener created by useEventListener, the variables
dragStart, dragging, bgRef, setPointerCapture and DRAG_THRESHOLD_PX when
locating and updating the code.

In `@src/components/appMode/layout/PanelHeader.vue`:
- Around line 16-25: The prop types in the defineProps block for PanelHeader.vue
declare expandLabel and collapseLabel as optional which allows aria-label to be
undefined on the icon-only chevron button; make expandLabel and collapseLabel
required by removing the optional markers so the prop contract enforces callers
to supply accessible names (update the defineProps generic to require
expandLabel: string and collapseLabel: string and adjust any related JSDoc
comment if present), then run typecheck to find and fix any call sites that must
now pass those props.

In `@src/renderer/extensions/linearMode/LinearPreview.vue`:
- Around line 115-131: loadWorkflow currently returns silently on failure which
allows rerun (and the reuse-parameters handler) to proceed and queue the wrong
graph; change loadWorkflow to return a boolean success flag (true when the
workflow was actually loaded/applied, false on any early return or failure) and
update callers like rerun (and the reuse-parameters handler referenced around
lines 147-150) to await loadWorkflow(...) and early-return if it resolves to
false before calling runButtonClick or queuing actions; reference functions:
loadWorkflow, rerun, and the reuse-parameters handler so the callers only
proceed when loadWorkflow succeeds.

---

Nitpick comments:
In `@src/components/appMode/layout/cells/BatchCountCell.vue`:
- Around line 3-9: The file imports useI18n and destructures const { t } =
useI18n() but t is never used in script scope — it's only referenced in the
template; remove the useI18n import and the const { t } = useI18n() line from
BatchCountCell.vue and update template usages to call the global $t instead of t
so the component relies on the built-in template translator.

In `@src/utils/widgetSubtitleUtil.test.ts`:
- Around line 48-66: The size-pattern test in widgetSubtitleUtil.test.ts only
covers the plural 'dimensions' but the regex uses 'dimensions?' so add the
singular 'dimension' to the it.each array to ensure widgetSubtitle({ name, spec:
{ type: 'INT', name } }) returns 'size' for name 'dimension'; update the test
case that currently lists
['width','height','image_width','image_height','size','dimensions'] to include
'dimension' and run the tests for the widgetSubtitle function.
- Around line 1-19: Add an explicit mock for the i18n module your util depends
on instead of relying on global module initialization: in the widgetSubtitle
test file, mock the '@/i18n' export (the t function) before using widgetSubtitle
so t('linearMode.widgetSubtitle.text') and friends return the expected strings;
update the test to call vi.mock('@/i18n', ...) (or the equivalent mocking
helper) to provide a deterministic t implementation for keys used by
widgetSubtitle, then run the existing assertions against widgetSubtitle({ name:
'unmapped', spec: { type: specType, name: '' } }) to keep the test
self-contained.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03301635-15b7-42e9-a919-0c47a604164a

📥 Commits

Reviewing files that changed from the base of the PR and between d3ab2be and 7a5e375.

📒 Files selected for processing (79)
  • browser_tests/fixtures/helpers/BuilderFooterHelper.ts
  • browser_tests/fixtures/selectors.ts
  • browser_tests/tests/builderSaveFlow.spec.ts
  • docs/adr/0009-app-mode-builder-unification.md
  • docs/adr/README.md
  • packages/design-system/src/css/style.css
  • src/assets/css/style.css
  • src/components/LiteGraphCanvasSplitterOverlay.vue
  • src/components/appMode/AppModeToolbar.vue
  • src/components/appMode/layout/AppChrome.vue
  • src/components/appMode/layout/LayoutView.vue
  • src/components/appMode/layout/OutputWindow.vue
  • src/components/appMode/layout/OutputWindowList.vue
  • src/components/appMode/layout/PanelHeader.vue
  • src/components/appMode/layout/PreviewCard.vue
  • src/components/appMode/layout/cells/BatchCountCell.vue
  • src/components/appMode/layout/cells/FeedbackCell.vue
  • src/components/appMode/layout/cells/IconCell.vue
  • src/components/appMode/layout/cells/InputCell.vue
  • src/components/appMode/layout/cells/JobQueueCell.vue
  • src/components/appMode/layout/cells/ModeToggleCell.vue
  • src/components/appMode/layout/cells/RunCell.vue
  • src/components/appMode/layout/panels/FloatingPanel.vue
  • src/components/appMode/layout/panels/PanelBlockList.vue
  • src/components/appMode/layout/panels/PanelDragPreview.vue
  • src/components/appMode/layout/panels/panelPresetClasses.ts
  • src/components/appMode/layout/panels/panelTypes.ts
  • src/components/appMode/layout/panels/useAppPanelLayout.ts
  • src/components/appMode/layout/panels/useBlockDrag.ts
  • src/components/appMode/layout/panels/useFlipReorder.ts
  • src/components/appMode/layout/panels/usePanelDrag.ts
  • src/components/appMode/layout/panels/usePanelResize.ts
  • src/components/appMode/layout/panels/usePointerDrag.ts
  • src/components/builder/AppBuilder.vue
  • src/components/builder/AppModeWidgetList.vue
  • src/components/builder/BuilderBackdrop.vue
  • src/components/builder/BuilderFooterToolbar.vue
  • src/components/builder/BuilderMenu.vue
  • src/components/builder/BuilderPanel.vue
  • src/components/builder/BuilderToolbar.vue
  • src/components/builder/IoItem.vue
  • src/components/builder/StepBadge.vue
  • src/components/builder/StepLabel.vue
  • src/components/builder/types.ts
  • src/components/graph/GraphCanvas.vue
  • src/components/maskeditor/BrushSettingsPanel.vue
  • src/components/maskeditor/ColorSelectSettingsPanel.vue
  • src/components/maskeditor/ImageLayerSettingsPanel.vue
  • src/components/maskeditor/PaintBucketSettingsPanel.vue
  • src/components/queue/QueueNotificationBanner.vue
  • src/components/ui/ZoomPane.vue
  • src/locales/en/main.json
  • src/platform/workflow/management/stores/comfyWorkflow.ts
  • src/platform/workflow/validation/schemas/workflowSchema.test.ts
  • src/platform/workflow/validation/schemas/workflowSchema.ts
  • src/renderer/extensions/linearMode/AppInput.vue
  • src/renderer/extensions/linearMode/AppModeWordmark.vue
  • src/renderer/extensions/linearMode/AppOutput.vue
  • src/renderer/extensions/linearMode/DragReorderDemo.vue
  • src/renderer/extensions/linearMode/ImagePreview.vue
  • src/renderer/extensions/linearMode/LatentPreview.vue
  • src/renderer/extensions/linearMode/LinearArrange.vue
  • src/renderer/extensions/linearMode/LinearPreview.vue
  • src/renderer/extensions/linearMode/LinearWelcome.test.ts
  • src/renderer/extensions/linearMode/LinearWelcome.vue
  • src/renderer/extensions/linearMode/MediaOutputPreview.vue
  • src/renderer/extensions/linearMode/SelectionChrome.vue
  • src/renderer/extensions/linearMode/VideoPreview.vue
  • src/renderer/extensions/linearMode/outputWindowStore.test.ts
  • src/renderer/extensions/linearMode/outputWindowStore.ts
  • src/renderer/extensions/linearMode/useOutputWindowSync.ts
  • src/stores/appModeStore.test.ts
  • src/stores/appModeStore.ts
  • src/types/widgetTypes.ts
  • src/utils/nodeTitleUtil.ts
  • src/utils/widgetSubtitleUtil.test.ts
  • src/utils/widgetSubtitleUtil.ts
  • src/views/GraphView.vue
  • src/views/LinearView.vue
💤 Files with no reviewable changes (5)
  • browser_tests/fixtures/selectors.ts
  • src/components/builder/types.ts
  • src/components/ui/ZoomPane.vue
  • src/components/builder/IoItem.vue
  • browser_tests/fixtures/helpers/BuilderFooterHelper.ts

Comment thread src/components/appMode/layout/AppChrome.vue
Comment thread src/components/appMode/layout/LayoutView.vue
Comment thread src/components/appMode/layout/LayoutView.vue
Comment on lines +33 to +52
onUpdated(() => {
const skip = skipKey?.() ?? null
const els =
containerEl.value?.querySelectorAll<HTMLElement>('[data-flip-key]')
if (!els) return
for (const el of els) {
const key = el.dataset.flipKey
if (!key || key === skip) continue
const prev = prevRects.get(key)
if (!prev) continue
const next = el.getBoundingClientRect()
const dx = prev.left - next.left
const dy = prev.top - next.top
// Skip sub-pixel deltas — would jiggle on unrelated updates.
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue
el.animate(
[{ transform: `translate(${dx}px, ${dy}px)` }, { transform: 'none' }],
{ duration: durationMs, easing: 'ease' }
)
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 9, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Honor reduced-motion when replaying FLIP animations.

This path animates every reorder via WAAPI, so users who have prefers-reduced-motion: reduce still get list-slide motion on every patch. A small guard here keeps the reorder behavior while avoiding forced animation for motion-sensitive users.

🤖 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 `@src/components/appMode/layout/panels/useFlipReorder.ts` around lines 33 - 52,
The FLIP replay in onUpdated ignores users' reduced-motion preference; add a
guard using window.matchMedia('(prefers-reduced-motion: reduce)').matches (or
equivalent) and skip WAAPI animations when it returns true. In practice, check
this once at the start of the onUpdated callback (or before the animation loop)
and if reduced-motion is enabled simply return or continue so the loop does not
call el.animate; reference onUpdated, skipKey, containerEl, prevRects and
durationMs to find where to insert the check.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — prefers-reduced-motion is a real concern but FLIP reordering is fundamental to the panel UX. The right fix is to short-circuit FLIP into an instant snap when reduced-motion is set, which needs a small composable refactor. Worth a follow-up but outside this PR's scope.

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.

The PR description images show:

  • Image 1: App view with two image cards and a floating “Output” window showing step 13/30 progress; a right panel edits prompt/size/filename.
  • Image 2: A dense grid of eye images; right panel “green-eyes-001” with prompt and 512×512 controls.
  • Image 3: “anima-bunny-001” session; multiple bunny outputs with a small floating progress bar; right prompt panel.
  • Image 4: Large gallery of cyberpunk eyes; floating Output shows step 20/20; right panel uses a tall 512×768 size.
  • Image 5: “bunny-anima-001” with 1024×1024 main image, five variants, and an “Input” panel on the right.

@eliheuer Got it—agree on deferring. I’ll queue a follow-up to short-circuit FLIP when reduced motion is enabled so reorders “snap” without animation.

If helpful later, here’s the minimal composable change that keeps behavior identical but skips WAAPI when users prefer reduced motion. It doesn’t alter panel UX; it only gates the animate() call.

// src/components/appMode/layout/panels/useFlipReorder.ts
function prefersReducedMotion(): boolean {
  try {
    return typeof window !== 'undefined' &&
      typeof window.matchMedia === 'function' &&
      window.matchMedia('(prefers-reduced-motion: reduce)').matches
  } catch {
    return false
  }
}

onBeforeUpdate(() => {
  prevRects.clear()
  if (prefersReducedMotion()) return
  const els = containerEl.value?.querySelectorAll<HTMLElement>('[data-flip-key]')
  if (!els) return
  for (const el of els) {
    const key = el.dataset.flipKey
    if (key) prevRects.set(key, el.getBoundingClientRect())
  }
})

onUpdated(() => {
  if (prefersReducedMotion()) return // instant snap: no WAAPI
  const skip = skipKey?.() ?? null
  const els = containerEl.value?.querySelectorAll<HTMLElement>('[data-flip-key]')
  if (!els) return
  for (const el of els) {
    const key = el.dataset.flipKey
    if (!key || key === skip) continue
    const prev = prevRects.get(key)
    if (!prev) continue
    const next = el.getBoundingClientRect()
    const dx = prev.left - next.left
    const dy = prev.top - next.top
    if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue
    el.animate(
      [{ transform: `translate(${dx}px, ${dy}px)` }, { transform: 'none' }],
      { duration: durationMs, easing: 'ease' }
    )
  }
})

Want me to open a follow-up GitHub issue now with:

  • Title: Respect prefers-reduced-motion in useFlipReorder (instant snap instead of WAAPI)
  • Summary/scope: Gate FLIP replay behind prefers-reduced-motion; keep DOM reorders, skip animations.
  • Acceptance criteria:
    • With matchMedia('(prefers-reduced-motion: reduce)') true, no calls to Element.animate during reorder; items snap instantly.
    • With it false, current animations remain unchanged.
    • Unit test stubs matchMedia and asserts animate not called when reduced.
      If yes, I’ll file it and backlink to this PR and your comment, assigning it to you unless you prefer someone else.

✏️ Learnings added
Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/useFlipReorder.ts:33-52
Timestamp: 2026-05-09T03:26:06.074Z
Learning: In Comfy-Org/ComfyUI_frontend, FLIP reorder animations (useFlipReorder.ts) are core to panel UX. For accessibility, the correct approach is to honor prefers-reduced-motion by skipping WAAPI (instant snap) rather than removing reordering. Treat as a follow-up item when out of scope for a PR.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.

Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 11531
File: src/composables/maskeditor/brushDrawingUtils.ts:14-14
Timestamp: 2026-04-22T04:06:01.353Z
Learning: In the Comfy-Org/ComfyUI_frontend codebase, `knip` is used to detect unused exports. When reviewing TypeScript/TSX code, avoid recommending or introducing exported types/functions/constants that have no concrete external consumers (e.g., they’re only used within the same module). Only recommend exporting when there is an actual external usage outside the module. If something might be needed externally later, keep it non-exported for now and defer exporting until an external consumer is added.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/useAppPanelLayout.ts:0-0
Timestamp: 2026-05-07T11:21:56.210Z
Learning: In the Vue 3/TypeScript frontend (ComfyUI_frontend), when LiteGraph mutates/reuses graph node arrays in place, Vue reactivity may not trigger if you only mirror/mutate the existing array. When updating state backed by a shallowRef (e.g., for derived panel/layout data), assign a new array reference when mirroring nodes—e.g., `graphNodes.value = [...(app.rootGraph?.nodes ?? [])]`—so recomputed values like `inputEntries`/`panelRows` rerun on reconfigure.

Comment thread src/components/builder/BuilderBackdrop.vue Outdated
Comment thread src/renderer/extensions/linearMode/LinearWelcome.vue
Comment thread src/renderer/extensions/linearMode/outputWindowStore.ts Outdated
Comment thread src/renderer/extensions/linearMode/SelectionChrome.vue
Comment thread src/stores/appModeStore.ts Outdated
Comment thread src/views/LinearView.vue
Comment on lines +124 to +129
<Splitter
v-if="activeTab"
class="flex-1 border-none bg-transparent"
state-key="Comfy.LinearView.SidebarPanelSplitter"
state-storage="local"
@resizestart="$event.originalEvent.preventDefault()"
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 9, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and examine the target lines
if [ -f "src/views/LinearView.vue" ]; then
  echo "File exists. Examining lines 120-135:"
  sed -n '120,135p' src/views/LinearView.vue
else
  echo "File not found"
  find . -name "LinearView.vue" -type f
fi

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 699


🏁 Script executed:

# Get more context around the splitter to understand the panel structure
sed -n '100,170p' src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2359


🏁 Script executed:

# Search for sidebarOnLeft usage in the file
grep -n "sidebarOnLeft" src/views/LinearView.vue | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 936


🏁 Script executed:

# Check how SplitterPanel elements are arranged and ordered
grep -n "SplitterPanel\|Splitter" src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 871


🏁 Script executed:

# Find and examine the useStablePrimeVueSplitterSizer composable
find . -name "*useStablePrimeVueSplitterSizer*" -o -name "*SplitterSizer*" | head -10

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 179


🏁 Script executed:

# Search for the composable definition
rg -l "useStablePrimeVueSplitterSizer" --type ts --type js

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 200


🏁 Script executed:

# Check the composable content
find . -path "*/composables/*" -name "*.ts" | xargs grep -l "useStablePrimeVueSplitterSizer" 2>/dev/null

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 179


🏁 Script executed:

# Examine the useStablePrimeVueSplitterSizer composable
cat -n src/composables/useStablePrimeVueSplitterSizer.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2321


🏁 Script executed:

# Also check the test file to understand the expected behavior
cat -n src/composables/useStablePrimeVueSplitterSizer.test.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4553


🏁 Script executed:

# Check how useStablePrimeVueSplitterSizer is called in LinearView.vue
sed -n '80,95p' src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 604


🏁 Script executed:

# Find the panel refs that are passed to the composable
grep -n "const.*Ref\|template.*ref\=" src/views/LinearView.vue | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 438


🏁 Script executed:

# Get a broader view of the composable usage and panel refs setup
sed -n '1,100p' src/views/LinearView.vue | tail -50

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1889


🏁 Script executed:

# Check if the inner Splitter (lines 124-163) uses the composable at all
# Search for refs or state-key handling in that section
sed -n '124,163p' src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1390


🏁 Script executed:

# Check if there are any template refs for the inner splitter panels
grep -n "ref=\".*Panel\|#.*Panel" src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 127


🏁 Script executed:

# Get the complete section including all splitters and their configurations
sed -n '120,200p' src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2708


🏁 Script executed:

# Check if there's any other state management or key handling for the inner splitter
grep -B5 -A15 "state-key=\"Comfy.LinearView.SidebarPanelSplitter\"" src/views/LinearView.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 814


🏁 Script executed:

# Verify the panel structure and conditional rendering more carefully
sed -n '124,163p' src/views/LinearView.vue | grep -n "SplitterPanel\|v-if"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 328


Key the persisted splitter state by sidebar side.

sidebarOnLeft swaps the panel order in the Splitter, but state-key remains constant. PrimeVue restores splitter sizes by panel index, so a saved sidebar width can be reapplied to the wrong panel after the side flips. Make the state-key side-specific so restored widths always belong to the correct panel.

Proposed fix
-        state-key="Comfy.LinearView.SidebarPanelSplitter"
+        :state-key="
+          `Comfy.LinearView.SidebarPanelSplitter.${
+            sidebarOnLeft ? 'left' : 'right'
+          }`
+        "
🤖 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 `@src/views/LinearView.vue` around lines 124 - 129, The Splitter's persisted
state-key is constant so when sidebarOnLeft toggles the panel order the saved
sizes get applied to the wrong panel; update the Splitter usage to include
sidebar side information in the state-key (e.g., append or interpolate
sidebarOnLeft into the state-key string) so the key is unique per side, ensuring
the Splitter component's state-key reflects sidebarOnLeft and thus restores
widths to the correct panel.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Deferred — LinearView's sidebar handling at this line interacts with several already-sidebar-aware computeds (showRightBuilder, showLeftBuilder, etc.). The remaining edge case is small enough to address alongside the broader sidebar-location pass that would also clean up LinearArrange.

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.

The PR description images show:

  • Image 1 (App view with cyberpunk eye): A grid workspace with two image cards, a floating “Output” progress window (step 13/30, 32s), and a right properties panel for node image_anima_preview--005 with prompt, width/height and filename_prefix controls.
  • Image 2 (Grid gallery “green-eyes-001”): Many eye images in a masonry grid; right panel shows prompt, Number of runs, Run, and size controls.
  • Image 3 (“anima-bunny-001”): Multiple bunny outputs; a small floating progress overlay (step 22/30, 14s); right panel with prompt and size fields.
  • Image 4 (Large gallery, tall aspect): Dense grid of eyes; floating “Output” window showing step 20/20; right panel (width 512, height 768).
  • Image 5 (“bunny-anima-001”, 1024x1024): Large selected image left, variations middle, and an “Input” panel right with a 1024x1024 prompt.

@eliheuer Thanks—acknowledging the deferral. Bundling this with the broader Comfy.Sidebar.Location pass (and the LinearArrange cleanup) makes sense and aligns with the stored guideline we have about respecting the sidebar location across views.

For that future pass, here’s the tiny change we’ll queue up to avoid cross‑applying saved widths when the sidebar flips in LinearView (around Line 124 where the Splitter is created in the showLayoutView branch):

<Splitter
  v-if="activeTab"
  class="flex-1 border-none bg-transparent"
  :state-key="`Comfy.LinearView.SidebarPanelSplitter.${sidebarOnLeft ? 'left' : 'right'}`"
  state-storage="local"
  `@resizestart`="$event.originalEvent.preventDefault()"
>

Proposed acceptance criteria for the follow-up:

  • Toggling Comfy.Sidebar.Location left/right preserves the last width for each side independently when returning to LinearView with an active tab.
  • Border placement and panel order already handled by existing computeds remain correct.
  • No changes to the builder Splitter behavior or stored widths.

Light test plan (later):

  • Manual: set widths with sidebar on left, reload; flip to right, set different width, reload; flip back—verify each side restores its own width.
  • Optional Playwright: assert localStorage keys diverge by suffix (.left/.right) and sizes restore after reload.

If you’d like, I can open a small tracking issue for this and reference this PR/comment, assigning it to you. Should I go ahead and create that GitHub issue now?


🧠 Learnings used
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 7297
File: src/components/actionbar/ComfyActionbar.vue:33-43
Timestamp: 2025-12-09T21:40:12.361Z
Learning: In Vue single-file components, allow inline Tailwind CSS class strings for static classes and avoid extracting them into computed properties solely for readability. Prefer keeping static class names inline for simplicity and performance. For dynamic or conditional classes, use Vue bindings (e.g., :class) to compose classes.

Applies to all Vue files in the repository (e.g., src/**/*.vue) where Tailwind utilities are used for static styling.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.vue:17-17
Timestamp: 2025-12-16T22:26:49.463Z
Learning: In Vue 3.5+ with <script setup>, when using defineProps<Props>() with partial destructuring (e.g., const { as = 'button', class: customClass = '' } = defineProps<Props>() ), props that are not destructured (e.g., variant, size) stay accessible by name in the template scope. This pattern is valid: you can destructure only a subset of props for convenience while referencing the remaining props directly in template expressions. Apply this guideline to Vue components across the codebase (all .vue files).

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:38.870Z
Learning: Tailwind CSS v4 safe utilities (e.g., items-center-safe, justify-*-safe, place-*-safe) are allowed in Vue components under src/ and in story files. Do not flag these specific safe variants as invalid when reviewing code in src/**/*.vue or related stories.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:46.862Z
Learning: In the ComfyUI_frontend repository, for Vue components, do not add aria-label to buttons that have visible text content (e.g., buttons containing <span> text). The visible text provides the accessible name. Use aria-label only for elements without visible labels (e.g., icon-only buttons). If a button has no visible label, provide a clear aria-label or associate with an aria-labelledby describing its action.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/components/graph/selectionToolbox/ColorPickerButton.vue:15-18
Timestamp: 2025-12-21T01:06:02.786Z
Learning: In Comfy-Org/ComfyUI_frontend, in Vue component files, when a filled icon is required (e.g., 'pi pi-circle-fill'), you may mix PrimeIcons with Lucide icons since Lucide lacks filled variants. This mixed usage is acceptable when one icon library does not provide an equivalent filled icon. Apply consistently across Vue components in the src directory where icons are used, and document the rationale when a mixed approach is chosen.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7649
File: src/platform/cloud/subscription/components/PricingTable.vue:185-201
Timestamp: 2025-12-22T21:36:08.369Z
Learning: In Vue components, avoid creating single-use variants for common UI components (e.g., Button and other shared components). Aim for reusable variants that cover multiple use cases. It’s acceptable to temporarily mix variant props with inline Tailwind classes when a styling need is unique to one place, but plan and consolidate into shared, reusable variants as patterns emerge across the codebase.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7893
File: src/components/button/IconGroup.vue:5-6
Timestamp: 2026-01-08T02:26:18.357Z
Learning: In components that use the cn utility from '@/utils/tailwindUtil' with tailwind-merge, rely on the behavior that conflicting Tailwind classes are resolved by keeping the last one. For example, cn('base-classes bg-default', propClass) will have any conflicting background class from propClass override bg-default. This additive pattern is intentional and aligns with the shadcn-ui convention; ensure you document or review expectations accordingly in Vue components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8195
File: src/platform/assets/components/MediaAssetFilterBar.vue:16-16
Timestamp: 2026-01-21T01:28:27.626Z
Learning: In Vue templates (Vue 3.4+ with the build step), when binding to data or props that are camelCase (e.g., mediaTypeFilters), you can use kebab-case in the template bindings (e.g., :media-type-filters). This is acceptable and will resolve to the corresponding camelCase variable. Do not require CamelCase in template bindings.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8090
File: src/platform/assets/components/modelInfo/ModelInfoField.vue:8-11
Timestamp: 2026-01-22T02:28:58.105Z
Learning: In Vue 3 script setup, props defined with defineProps are automatically available by name in the template without destructuring. Destructuring the result of defineProps inside script can break reactivity; prefer accessing props by name in the template. If you need to use props in the script, reference them via the defined props object rather than destructuring, or use toRefs when you intend to destructure while preserving reactivity.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8497
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue:223-236
Timestamp: 2026-02-01T21:10:29.567Z
Learning: In Vue single-file components, do not review or require edits to comments. Favor self-documenting code through clear naming, strong types, and explicit APIs. If a comment is misleading or outdated, consider removing it, but avoid suggesting adding or fixing comments. This guideline aligns with preferring code clarity over comment maintenance across all .vue files.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8592
File: src/components/topbar/WorkflowExecutionIndicator.vue:28-28
Timestamp: 2026-02-03T21:35:40.889Z
Learning: In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script. This avoids unnecessary imports when t is not used in the script. If you need i18n in the script (Composition API), only then use useI18n and access t from its returned object. Ensure this pattern applies to all Vue components with template-only i18n usage.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 8548
File: src/components/common/TreeExplorerV2Node.vue:9-47
Timestamp: 2026-02-07T14:47:06.751Z
Learning: In all Vue files that render Reka UI context menu components (ContextMenuRoot, ContextMenuTrigger, etc.), avoid dynamic component wrapping with <component :is="...">. Use conditional rendering with v-if / v-else to render separate branches, even if it results in some template duplication. This improves readability and maintainability across files that use these components.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:47.113Z
Learning: When destructuring reactive properties from Pinia stores in Vue components, use storeToRefs() to preserve reactivity. Example: const store = useCanvasStore(); const { canvas } = storeToRefs(store); access as canvas.value (e.g., canvas.value). Ensure you import storeToRefs from 'pinia' and use it wherever you destructure store properties in the setup function.

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 8775
File: src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue:62-68
Timestamp: 2026-02-10T17:59:25.893Z
Learning: In Vue 3 single-file components, code defined outside the script setup scope cannot access variables or helpers defined inside setup. If a default function (e.g., defineModel) is hoisted outside script setup, it will not be able to call setup helpers or reference setup-scoped variables. Place such logic inside setup or expose necessary values via returns/defineExpose to be accessible.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9085
File: src/components/sidebar/tabs/NodeLibrarySidebarTabV2.vue:45-50
Timestamp: 2026-02-27T09:39:47.145Z
Learning: In Vue components that use Reka UI primitives such as DropdownMenuTrigger and SelectTrigger with as-child, it is acceptable to render a raw <button> when the styling is context-specific and there is no existing Button variant to map to. The as-child pattern preserves accessibility while enabling composition. Do not create single-use Button variants for these cases; prefer inline styling with a raw button for clarity and to avoid unnecessary abstraction. This guideline applies to all Vue components (files ending in .vue) in projects using Reka UI.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 9749
File: src/platform/assets/components/MediaAssetContextMenu.vue:88-93
Timestamp: 2026-03-11T19:12:43.621Z
Learning: For Vue components using PrimeVue ContextMenu, the root element merges all passthrough attributes via v-bind="ptmi('root')". Passing an id through :pt="{ root: { id: someId } }" will reliably set the root element's id, allowing document.getElementById(someId) to reference the overlay. Do not flag this pattern as unreliable in reviews for components using PrimeVue ContextMenu with pt.root.id passthrough (e.g., src/platform/assets/components/MediaAssetContextMenu.vue).

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:17.291Z
Learning: Maintain narrowly scoped PRs by limiting changes to the stated fix within a single PR. Do not bundle cross-file refactors (e.g., adding concurrency guards across composables like useBillingContext) into a targeted bug-fix PR; instead propose these as separate follow-up issues or PRs. This guidance applies to Vue components (e.g., CloudSubscriptionRedirectView.vue) and other files across the repository. When a change touches multiple files or concerns broader architecture, split into separate, well-scoped PRs to improve reviewability and risk assessment.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/FeedbackCell.vue:0-0
Timestamp: 2026-04-28T07:02:16.613Z
Learning: In Vue SFCs (files under src/**/*.vue), avoid using Tailwind “important” modifiers (`!`, e.g., `!bg-*`, `!text-*`). For overrides that are truly needed, prefer defining/overriding styles in the component’s scoped <style> block using Vue’s `:deep()` selectors instead of relying on Tailwind `!` (e.g., follow the approach used in FeedbackCell.vue and ModeToggleCell.vue).

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/platform/cloud/subscription/components/PricingTable.vue:0-0
Timestamp: 2026-05-07T11:23:16.180Z
Learning: In ComfyUI_frontend Vue components, do not use the Tailwind foreground alias utility classes like `text-success-foreground` (they are intentionally descoped/unknown to Tailwind in App Mode). Instead, use arbitrary-value CSS tokens such as `text-(--success-foreground)` (or an explicit semantic token like `text-(--success-background)` when the context calls for background vs foreground). During review, flag suggestions that use `text-*-foreground` aliases and prefer `text-(--<token>)` forms.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/views/LinearView.vue:124-163
Timestamp: 2026-05-08T23:31:33.868Z
Learning: In Comfy-Org/ComfyUI_frontend, any Vue view that renders a sidebar/extension panel must respect the Comfy.Sidebar.Location setting. When adding/branching layout logic (e.g., LinearView’s showLayoutView path), conditionally (1) reorder the Splitter panel(s) and (2) flip the sidebar border styling (e.g., border-r/border-l) so the sidebar/extension appears on the configured side. Ensure this behavior is implemented consistently across all views in src/views/**/*.vue that render such panels.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/builder/BuilderToolbar.vue:59-77
Timestamp: 2026-05-08T23:31:37.825Z
Learning: If the app does not support runtime locale switching mid-session, avoid reactive i18n updates (e.g., don’t set up locale-dependent reactive/computed/watch-based translations). In Vue components, it’s acceptable to call i18n’s `t()` during `setup` and cache the resulting strings (such as UI/step titles), so the UI remains consistent without attempting reactive locale changes until locale switching is actually implemented.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/cells/InputCell.vue:95-101
Timestamp: 2026-05-08T23:32:07.123Z
Learning: In Comfy-Org/ComfyUI_frontend, treat the Tailwind-like responsive variant prefix `not-md:` as an intentional, repo-established convention. During code review, do not suggest replacing `not-md:` with `max-md:` (e.g., for responsive sizing used by NodeWidgets); consider `not-md:` valid wherever class names are defined in `src/**/*.vue` and top-level `*.ts` files.

Learnt from: eliheuer
Repo: Comfy-Org/ComfyUI_frontend PR: 11317
File: src/components/appMode/layout/panels/PanelBlockList.vue:143-147
Timestamp: 2026-05-08T23:35:15.715Z
Learning: In the ComfyUI frontend (Vue components), it’s acceptable to use the CSS `field-sizing: content` on `<textarea>` elements. Rely on the native fallback behavior in browsers that don’t support it (i.e., no auto-sizing) and do not add a JavaScript polyfill for this feature unless product requirements explicitly change to require auto-sizing across unsupported browsers.

…bugs)

Sidebar-location follow-ons (extending the work from 2f7e90a):
- AppChrome (builder variant), BuilderBackdrop, and LinearWelcome
  now read `Comfy.Sidebar.Location` and reserve the gutter on the
  matching side. Previously each hardcoded the left gutter.

Workspace-scope follow-on (extending 1a1025b):
- LayoutView's `handlePointerDown` now bails when the event target
  isn't inside `bgRef`, matching the wheel-handler scoping. Pointer
  events from the floating panel + chrome no longer arm a workspace
  pan-drag.

Accessibility follow-on (extending c668618's keyboard semantics):
- SelectionChrome now has a localized `aria-label` ("Select" /
  "Deselect" depending on `isSelected`, with an optional override
  prop). Adds new `linearMode.selection.{select,deselect}` keys.

Real bugs:
- LayoutView's `dashboardInsetsKey` now encodes panel y/h alongside
  x/w. A panel dragged from float-tl to float-bl previously didn't
  retrigger `relayoutDashboard()`.
- `appModeStore.loadSelections` now resets panel state to defaults
  (`right-dock`, `false`, `8`, `[]`) when the loaded workflow has
  no `extra.linearData.layout`. Previously the store leaked the
  prior workflow's panel preset / collapse / width / rows.
- `outputWindowStore.nextSpawnPosition` now snaps coordinates
  *before* testing for overlap. Earlier the unsnapped candidate
  passed overlap-check, then snap drift could place the new tile
  on top of a neighbor.
- `AppInput` collapses its two-branch render (wrapped vs bare slot)
  into a single render path with conditional wrapper styling
  (`contents` when off). Prevents the slotted widget tree from
  remounting on every `showSelection` flip — was dropping focus
  and widget-local state.

53 store tests still pass; typecheck + lint clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants