Consolidate the Parameters-panel drag-and-drop wiring into SectionWidgets,
which now owns the imperative DraggableList lifecycle and emits a typed
`@reorder { fromIndex, toIndex }` event.
TabSubgraphInputs previously passed UI-list indices straight to
reorderSubgraphInputAtIndex, which interprets them as indices into
subgraph.inputs. Because the rendered list is a filtered + augmented view
(filters out non-promoted inputs, appends virtual host widgets), the
indices misaligned whenever any non-promoted input or extra widget was
present — moving the wrong row.
Both Subgraph reorder paths now go through reorderSubgraphInputsByWidgetOrder,
which resolves positions by widget identity, matching the Subgraph Editor.
TabGlobalParameters shares the same SectionWidgets reorder contract.
Amp-Thread-ID: https://ampcode.com/threads/T-019e4cdc-434e-7659-9e1f-b29e3ad8ac14
Co-authored-by: Amp <amp@ampcode.com>
Per ADR_0009, linking to a SubgraphInput must be indistinguishable from
a regular link. buildSlotMetadata derived 'linked' via getNodeById on
the link origin, which returns null for the SUBGRAPH_INPUT_ID sentinel
(SubgraphInputNode is not in _nodes_by_id). Widgets stayed enabled and
no slot dot rendered in Vue mode.
Use input.link != null directly, matching the origin-agnostic check in
LGraphNode.updateComputedDisabled used by canvas mode.
Amp-Thread-ID: https://ampcode.com/threads/T-019e4caf-85c8-75d8-86c2-5da97285a81a
Co-authored-by: Amp <amp@ampcode.com>
Regression test ensuring that after `promoteValueWidgetViaSubgraphInput`
creates the SubgraphInput-to-slot link, `updateComputedDisabled()` flips
the interior widget's `computedDisabled` to true.
This is the input-side guarantee that the `BaseDOMWidgetImpl.isVisible()`
fix in the previous commit relies on to hide a promoted DOM widget so
the link connection dot becomes visible.
Amp-Thread-ID: https://ampcode.com/threads/T-019e4c28-5668-7045-b744-da018fd06d25
Co-authored-by: Amp <amp@ampcode.com>
`BaseDOMWidgetImpl.isVisible()` only considered `hidden` and
`LGraphNode.isWidgetVisible`. When a widget's input slot was linked
(e.g. promoted to a SubgraphInput), the canvas dimmed the widget via
`computedDisabled` but the DOM overlay (multiline text, custom
component widgets, etc.) stayed fully rendered on top, hiding the
input connection dot beneath it.
`isVisible()` now also returns false when `computedDisabled` is true,
matching the canvas's intent and exposing the connection dot.
Amp-Thread-ID: https://ampcode.com/threads/T-019e4c28-5668-7045-b744-da018fd06d25
Co-authored-by: Amp <amp@ampcode.com>
Two related bugs in the ADR 0009 promotion path both stem from confusing
a nested PromotedWidgetView's deep `sourceWidgetName` with its immediate
boundary slot name.
- `getWidgetName` no longer peels back through `PromotedWidgetView`. It now
returns `w.name`, which already resolves to the immediate disambiguated
slot name (e.g. `text_1`) via the view's identity getter. Fixes the
context-menu predicate showing "Promote Widget" for a widget that is
already promoted when base names collide.
- `pruneDisconnected` treats a `SubgraphInput` as alive whenever any of
its links still resolves to an interior target slot that has a widget.
This stops the Subgraph Editor's onMount prune from wiping nested
promotions whose `widget.sourceNodeId` points at a deeply-nested
interior node not directly present in the host subgraph.
Both behaviours are covered by new regression tests in
`promotionUtils.test.ts` (`disambiguated nested promotion identity`).
Amp-Thread-ID: https://ampcode.com/threads/T-019e4c28-5668-7045-b744-da018fd06d25
Co-authored-by: Amp <amp@ampcode.com>
- reorderSubgraphInputs: validate orderedIndices is a permutation of 0..n-1
before mutating subgraph.inputs/subgraphNode.inputs; log and bail on
invalid input so slots aren't silently dropped or duplicated.
- _hydratePreviewExposures: treat an explicit `[]` on properties as the
user-cleared value instead of falling back to the legacy locator-keyed
store entry; only run the legacy migration when the property is truly
absent or not an array.
- Add round-trip test that serializes -> seeds a legacy locator entry ->
reconfigures and asserts the explicit empty array is preserved.
Amp-Thread-ID: https://ampcode.com/threads/T-019e4911-134e-708d-a511-fb356b87224f
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Spark 2.x requires SparkRenderer in scene tree; add it in SceneManager
and protect it in clearModel so model reloads don't dispose the splat
renderer.
- three 0.184 OrbitControls listens on ownerDocument; drop redundant
pointermove/up .stop in Load3D containers so the document listener can
receive events.
- Narrow Texture.image type for 0.184 strict typing.
## Summary
Allow asset/media FormDropdown searches to select the top filtered
result when the user presses Enter. This covers image, video, audio,
mesh, model-like asset selects, and other `WidgetSelectDropdown`-backed
media widgets.
## Implementation Scope
This PR implements a **top-result Enter shortcut** for the custom
asset/media dropdown path only:
- In scope: `WidgetSelectDropdown` -> `FormDropdown` asset/media
widgets.
- In scope: while the dropdown is open, single-select, and the search
text is non-empty, the first current search result becomes the Enter
candidate.
- In scope: pressing Enter in the search input selects that
candidate/top result through the existing selection path.
- In scope: candidate feedback for this shortcut, including visual
candidate styling and a polite screen-reader announcement for the
current top result.
- In scope: stale async search protection, empty-query/no-result no-op
behavior, multi-select guard behavior, and focus return to the trigger
after Enter selection closes the menu.
- Out of scope: plain combo widgets (`WidgetSelectDefault` /
`SelectPlus`). That path is PrimeVue-based and should be handled
separately from this focused asset-widget PR.
- Out of scope: full combobox/listbox keyboard navigation, including
Tab-to-list focus, ArrowUp/ArrowDown candidate movement, Home/End
behavior, scroll-to-active-item behavior, and a full ARIA
combobox/listbox refactor.
Follow-up arrow-key navigation should validate the interaction model
separately. This PR keeps the candidate state narrow and localized so
that future work can either extend it into movable active-item state or
replace it as part of a fuller combobox/listbox implementation.
## Changes
- **What**: Added an explicit Enter event from `FormSearchInput`, routed
it through the FormDropdown menu actions, and selected the current top
search result in `FormDropdown`.
- **What**: Kept the existing `computedAsync` + debounced filtering path
for normal typing, while Enter performs a one-off search against the
latest input before selecting. Stale async Enter results are ignored if
the query or item source changes before resolution.
- **What**: Prevented closed FormDropdown state from treating the full
unfiltered list as current search results, limited Enter-to-select to
single-select dropdowns, and made empty search Enter a no-op.
- **What**: Returned focus to the dropdown trigger after single-select
selection closes the menu.
- **What**: Added candidate styling for the first current FormDropdown
result while a search query is active so the Enter target is visible to
users.
- **What**: Added a polite screen-reader announcement for the current
top result candidate.
- **What**: Fixed the FormDropdownMenuActions `baseModelSelected` model
default to use a `Set` factory instead of a shared instance.
- **What**: Added unit coverage for the search Enter event, FormDropdown
selection behavior, focus return, debounce/Enter behavior, stale async
Enter protection, empty-query no-op behavior, closed-state stale result
protection, multi-select guard behavior, and candidate announcement
behavior. Added App Mode E2E coverage for asset FormDropdown Enter
selection.
- **What**: Extracted reusable app-mode dropdown fixture helpers and
updated the existing FormDropdown clipping test to use the shared
helper.
## Review Focus
Please focus review on the asset/media FormDropdown path, especially
`getTopSearchResult()`, the single-select/empty-query guards, stale
async search protection, trigger focus return after selection, and
candidate feedback in grid/list layouts.
The plain combo path and full arrow-key navigation are intentionally
left for separate follow-up work.
## Screenshots (if applicable)
https://github.com/user-attachments/assets/3eb3456d-93a3-4959-91a3-188f8116ccc9
Validation performed:
- Latest final-commit validation:
- `pnpm test:unit
src/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.test.ts
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.test.ts
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuActions.test.ts
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.test.ts`
- Commit hook: `pnpm exec stylelint ...`, `pnpm exec oxfmt --write ...`,
`pnpm exec oxlint --type-aware --fix ...`, `pnpm exec eslint --cache
--fix ...`, `pnpm typecheck`
- Push hook: `pnpm knip --cache`
- `git diff --check`
- Earlier branch validation for this flow:
- `pnpm install`
- `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm test:browser --
--project=chromium browser_tests/tests/appMode.spec.ts -g "Drag and
Drop|FormDropdown search Enter selects the top filtered item"
--reporter=list`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm test:browser --
--project=chromium browser_tests/tests/appMode.spec.ts -g "FormDropdown
search Enter selects the top filtered item" --reporter=list`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm test:browser --
--project=chromium browser_tests/tests/appModeDropdownClipping.spec.ts
-g "FormDropdown popup is not clipped" --reporter=list`
## Summary
Add entries to `MODEL_NODE_MAPPINGS` so the model browser's "Use" button
creates the correct loader node for two model directories introduced in
recent node-pack updates.
## Changes
- **What**: 2 new entries in
`src/platform/assets/mappings/modelNodeMappings.ts`:
- `geometry_estimation` → `LoadMoGeModel` / `model_name`
- `optical_flow` → `OpticalFlowLoader` / `model_name`
- **Breaking**: none
## Review Focus
- Node class names and input keys cross-checked against the published
node definitions:
- `LoadMoGeModel` is the MoGe geometry-estimation loader (companion
nodes: `MoGeInference`, `MoGeRender`, `MoGeContextStrandModel`)
- `OpticalFlowLoader` is the RAFT optical-flow loader
- Both directories accept a single model file via a COMBO widget on the
loader node
## Test plan
- [ ] Verify "Use" button works for each new model directory in the
model browser:
- One `geometry_estimation` model (e.g.
`moge_2_vitl_normal_fp16.safetensors`) → creates a `LoadMoGeModel` node
with the file preselected
- One `optical_flow` model → creates an `OpticalFlowLoader` node with
the file preselected
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12389-feat-add-model-to-node-mappings-for-geometry_estimation-and-optical_flow-3666d73d36508190981fcaf77f9d2ee4)
by [Unito](https://www.unito.io)
Auto-exposed previews would reappear after reload even when the user had
explicitly hidden them. The host's `properties.previewExposures` already
captures user intent, but `LGraph.configure` was unconditionally running
`autoExposeKnownPreviewNodes` after hydration, second-guessing the
serialized state.
- Gate `autoExposeKnownPreviewNodes` on `properties.previewExposures`
being undefined. A node that has never been saved (or comes from a
pre-PR workflow with no `previewExposures` property) still gets the
convenience auto-expose pass; once the property exists in any form,
including an empty array, the serialized state is authoritative.
- Always serialize `previewExposures` (including as `[]` when empty)
so "user cleared everything" is distinguishable from "never touched".
Amp-Thread-ID: https://ampcode.com/threads/T-019e4671-bc67-7039-b6c6-9f5b11a07e42
Co-authored-by: Amp <amp@ampcode.com>
Previously, the "Hide all" action in the subgraph editor skipped any
promoted widget that was a linked promotion, making the button a no-op
for the most common kind of promotion. Drop the isLinkedPromotion skip
so "Hide all" actually hides everything.
Amp-Thread-ID: https://ampcode.com/threads/T-019e4671-bc67-7039-b6c6-9f5b11a07e42
Co-authored-by: Amp <amp@ampcode.com>
Extract reorderSubgraphInputs into subgraphUtils as the single primitive
that keeps all slot/link bookkeeping in sync (inner origin_slot, outer
target_slot, inputs-reordered event). applySubgraphInputOrder now only
owns the promotion-specific widget-value preservation.
Amp-Thread-ID: https://ampcode.com/threads/T-019e4671-bc67-7039-b6c6-9f5b11a07e42
Co-authored-by: Amp <amp@ampcode.com>
Fixes 3 different bugs when making links to and from subgraph IO from
vue nodes
- When dragging a link from a node to a subgraph IO, there is no
feedback if a slot is not a valid connection target or if a slot is
actively hovered
- When a link is made from a subgraph IO to a node, the reactivity is
not triggered on the node to indicate a change of link state.
- When dragging a link from a subgraph IO to a node, the link would not
snap to the valid connection targets on nodes
- The fix for this one is not as thorough as I would like. It only
allows connections to the slot, not connections to the hovered widget.
We have two deeply disconnected linking systems and properly reconciling
them would be a multi-week project.
Resolves FE-561
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12281-Subgraph-io-fixes-3606d73d365081089f7ef19331c6d70a)
by [Unito](https://www.unito.io)
## Summary
Allow staging api/platform base URLs to be overridden by env vars so
non-cloud builds can target an alternate backend without source edits.
## Changes
- **What**: `BUILD_TIME_API_BASE_URL` / `BUILD_TIME_PLATFORM_BASE_URL`
in `src/config/comfyApi.ts` now read
`import.meta.env.VITE_STAGING_API_BASE_URL` /
`VITE_STAGING_PLATFORM_BASE_URL` first, falling back to the existing
`stagingapi.comfy.org` / `stagingplatform.comfy.org` constants. Vars
typed in `src/vite-env.d.ts` and documented in `.env_example`.
- **Breaking**: None. Defaults unchanged. The cloud-runtime override
path via the features endpoint (`comfy_api_base_url`,
`comfy_platform_base_url` in `RemoteConfig`) is untouched.
## Review Focus
Override only applies to the non-prod branch of the build-time ternary,
so prod builds (`USE_PROD_CONFIG=true`) cannot be redirected. Cloud
builds continue to resolve URLs at runtime via `remoteConfig` regardless
of these env vars.
## Note
Pre-commit `pnpm typecheck` fails on `origin/main` independently of this
change (`src/utils/nodeDefUtil.ts` and
`src/workbench/utils/nodeHelpUtil.ts` import non-existent exports from
`@/schemas/nodeDefSchema` / `@/types/nodeSource`). Verified by stashing
this PR's diff and re-running. Committed with `--no-verify`; please
address the underlying breakage separately.
This is a targeted small scope change to improve the availability for
converting group nodes into a subgraph.
The prior implementation would only apply on the litegraph context menu
option for converting a node to a subgraph. It failed to apply on any of
the other more common methods. The code for unpacking group nodes has
been moved directly into the setup for converting a group of nodes into
a subgraph and drastically simplified.
Of note, several other long lived bugs were found while working on this
fix, but they are out of scope for this targeted PR.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12356-On-subgraph-conversion-always-unpack-group-nodes-3666d73d365081d09774c00a851b8198)
by [Unito](https://www.unito.io)
## Summary
Import published media assets for shared workflows before loading the
graph so the first missing-media scan sees the user's newly imported
references instead of surfacing a false missing asset error. cc FE-773
## Changes
- **What**: Moves the shared workflow import step ahead of
`loadGraphData` for the copy-and-open flow, while still allowing the
workflow to open with a warning path if asset import fails.
- **What**: Clears the shared workflow URL intent consistently on
failure paths, including graph load failure after an import attempt, so
reloads do not repeatedly replay the same shared workflow side effects.
- **What**: Invalidates the input asset cache after published asset
import so graph loading and missing-media resolution can observe the
refreshed media state.
- **What**: Adds a global loading spinner while shared workflow asset
import and graph load are in progress, with `role="status"`,
`aria-live`, reduced-motion-safe animation, and body teleporting so it
stays visible above blocking UI.
- **What**: Adds stable TestIds for the shared workflow dialog and
updates existing shared workflow E2E selectors away from copy-dependent
role text.
- **What**: Adds a cloud E2E regression fixture and spec covering the
critical flow: shared URL opens the dialog, the user confirms asset
import, published media is imported before the public-inclusive input
asset scan, the workflow loads, the share query is removed, and missing
media UI is not surfaced.
- **Breaking**: None.
- **Dependencies**: None.
## Root Cause
Shared workflow graph loading triggered the missing-media pipeline
before the user-selected published media import had completed. Because
`include_public=true` does not include published assets, the pre-import
scan could classify shared media as missing even when the user was about
to import those assets into their own library.
## Review Focus
- The ordering in `useSharedWorkflowUrlLoader`: import published assets
first, then load the graph, while keeping import failure non-fatal for
workflow opening.
- The failure cleanup behavior: the shared URL/preserved query intent is
now cleared for graph load failures too, avoiding repeated
reload-triggered imports.
- The spinner behavior in `App.vue`: it uses the existing
`workspaceStore.spinner` boolean and intentionally keeps broader
ref-counted spinner ownership as follow-up work.
- The E2E sentinel in `sharedWorkflowMissingMedia.spec.ts`: it asserts
no public-inclusive input asset scan occurs before `/api/assets/import`,
then waits for a settling window to ensure the missing-media overlay
does not appear.
## Validation
- `pnpm format`
- `pnpm lint` (passed with existing unrelated warnings only)
- `pnpm typecheck`
- `pnpm test:unit`
- Commit hook: lint-staged formatting/linting, `pnpm typecheck`, `pnpm
typecheck:browser`
- Push hook: `pnpm knip --cache` (passed with existing tag hint only)
## Follow-Up
- Consider a ref-counted or scoped global spinner API so long-running
flows do not directly toggle `workspaceStore.spinner`.
- Consider separating shared workflow load status into orthogonal result
fields instead of encoding partial success in a single string union.
- Consider moving published asset import/cache invalidation behind an
asset-service-owned API boundary.
- Backend follow-up remains needed for `include_public=true` not
including published assets; this PR only removes the frontend false
positive when the user explicitly imports the shared media.
## Screenshots
Before
https://github.com/user-attachments/assets/dc790046-237c-4dd8-b773-2507f9a66650
After
https://github.com/user-attachments/assets/6517cd38-2c3d-4bfe-a990-35892b7e50aehttps://github.com/user-attachments/assets/d89dc3d3-75d9-4251-998b-0c354414e25b
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12333-fix-avoid-false-missing-media-errors-after-importing-shared-workflow-assets-3656d73d365081b38634dcb7625cfc32)
by [Unito](https://www.unito.io)
## Summary
Two fixes for the cloud LoadImage form dropdown:
1. **Cloud root-cause fix** — outputs now come from a single
`getAssetsByTag('output')` call instead of walking the jobs API and
per-job `resolveOutputAssetItems` detail fetches. Per Christian's [Slack
feedback](https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1778051260476369?thread_ts=1776716352.588229&cid=C0A4XMHANP3):
*"on cloud, we can just grab the assets with a single GET, filtering
with input or output tag."* Sidebar's job-stack UX is untouched.
2. **Local / defense-in-depth** — even when the watch+expansion path is
in play (still used by local), batch all in-flight
`resolveOutputAssetItems` for the current `media` snapshot via
`Promise.all`, committing once into `resolvedByJobId`. This kills the
progressive head-shift symptom even on the legacy path.
The first attempt at (1) (`6a1a083c9`, reverted in `c175962e8`) broke
select+load on cloud prod because the dropdown wrote `asset.name` (human
filename) into the widget value, but cloud's `/api/view` resolves output
files by **`asset_hash`** (the blake3-keyed filename). Verified against
cloud prod that every output row carries `asset_hash` and that cloud's
own `preview_url` is hash-keyed, not name-keyed. Re-introduced in
`d7693377` with the dropdown value derived from `asset.asset_hash ||
asset.name`, with the human filename retained as the display label.
- Fixes FE-227
## Cloud / local divergence — what this PR clarifies
| | input | output (this PR) |
| ------------ | ---------------------------------------------- |
-------------------------------------------------------- |
| **cloud** | `getAssetsByTag('input')` (already correct) |
**`getAssetsByTag('output')` (new)** |
| **local** | `/files/input` (FS-listing) | `getHistory` + per-job
expansion (unchanged) |
Both directions are now symmetric on cloud: tag-based listing,
hash-keyed values. Local stays on the legacy path because core ComfyUI
doesn't have the assets/tags model — that's the deeper convergence
Jacob/Luke flagged in the FE-556 thread (now BE-757), which is BE/Core
work and not this PR.
## Red-Green verification
| Commit | CI: Tests Unit | Purpose |
|--------|----------------|---------|
| `3e8d42e7` test | 🔴 [failure
(25413987208)](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25413987208)
| Asserts the head of the list does not shift while one of two
multi-output jobs is still resolving. |
| `fe2608d4` fix (atomic batch) | 🟢 [success
(25414246791)](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25414246791)
| Resolutions awaited via `Promise.all` and merged in one
`resolvedByJobId` update. |
| `6a1a083c` simplification (broken) | — | First attempt — used
`asset.name`, broke select+load on cloud prod. |
| `c175962e` revert | — | Rolled back the broken simplification while
diagnosis was in flight. |
| `d7693377` simplification (fixed) | pending | Re-introduces
`useFlatOutputAssets` and uses `asset.asset_hash` for the dropdown
value. Adds 7 unit tests covering hash-as-value, name-fallback,
pagination, dedupe, and error path. |
## screenshot/ video
### before
https://github.com/user-attachments/assets/239aa447-a260-4713-926c-04dd80a30408
### after
https://github.com/user-attachments/assets/d68228c6-33f5-4bf0-ad24-bb83c876fdc2
## Test plan
- [x] New `useFlatOutputAssets.test.ts` — 7 tests for tag-based
fetching, pagination, dedupe, error path.
- [x] `useWidgetSelectItems.test.ts` — atomic-batching regression test +
new tests asserting hash-as-value and local name-fallback. 35 tests
pass.
- [x] `WidgetSelectDropdown.test.ts` — 5 tests pass with the new
conditional source.
- [x] CI red on test-only commit, CI green on first fix commit.
- [ ] CI green on the simplification (re-introduce) commit.
- [ ] Manual verification on cloud build: open LoadImage → switch to
Outputs → scroll → list head stays stable; select an output → LoadImage
preview loads (was broken in `6a1a083c`, restored in `d7693377`).
## Summary
Classify `.ply` as 3D media so PLY outputs are surfaced by queue/assets
preview flows.
## Changes
- **What**: adds `.ply` to shared 3D extension detection and falls back
to the asset `/view` URL when opening 3D assets without `preview_url`.
- **Breaking**: none.
- **Dependencies**: none.
## Review Focus
- This is the tactical FE fix for FE-129; it intentionally does not
solve the broader 3D media vs load3d-loadable split.
- Assets sidebar 3D viewer still prefers `preview_url`, but now has a
usable fallback for assets that only have the normal asset URL.
FE-129
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12319-fix-classify-PLY-assets-as-3D-media-3646d73d365081218f0bde401b1601bd)
by [Unito](https://www.unito.io)
## Summary
Stops the Shape submenu (and any other PrimeVue nested submenu) from
being clipped behind the node context menu when the menu fits in the
viewport.
## Changes
- **What**: `constrainMenuHeight` in `NodeContextMenu.vue` now applies
`max-height` + `overflow-y: auto` to the root `<ul>` only when
`scrollHeight > availableHeight`. The common case keeps `overflow:
visible`.
- Added `browser_tests/tests/nodeContextMenuShapeSubmenu.spec.ts`
regression spec.
## Review Focus
Root cause: setting only `overflow-y: auto` on a `<ul>` coerces
`overflow-x` to a non-visible value per CSS spec (`If one of
overflow-x/overflow-y is visible and the other isn't, the visible value
is computed as auto`). PrimeVue `ContextMenuSub` renders submenus
in-tree as a nested `<ul>` with `position: absolute; left: 100%`, so the
implicit horizontal clip hides them entirely.
The pre-existing overflow scenario (#10824 / #10854) is unchanged — when
the menu actually overflows, the clamp still applies and
`nodeContextMenuOverflow.spec.ts` continues to verify scroll. Submenu
clipping in that overflow case is a known limitation, not introduced by
this PR.
Fixes FE-570
## screenshot
### AS IS
<img width="788" height="505" alt="Screenshot 2026-05-07 at 12 43 26 PM"
src="https://github.com/user-attachments/assets/36d34070-0c57-4385-a130-0394f22f282e"
/>
### TO BE
<img width="779" height="627" alt="Screenshot 2026-05-07 at 12 42 44 PM"
src="https://github.com/user-attachments/assets/00956729-763b-4787-822f-209e8ea42331"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12035-fix-keep-node-context-menu-overflow-visible-when-content-fits-3586d73d365081ad9aaec82f220d401c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
- Skip value-control on promoted widgets when host input is externally
linked (parity with src/scripts/widgets.ts), preventing randomize/
increment from overwriting upstream-driven seeds each queue
- Refresh useResolvedSelectedInputs on convert-to-subgraph and
subgraph-created root-graph events, fixing stale node resolution
in AppBuilder after graph restructure
- Remove forbidden `as unknown as IBaseWidget` cast in SubgraphEditor
by replacing the fake widget object with a local discriminated union
(ActiveRow = PromotedRow | PreviewRow); preview demote without a
real source widget now removes the exposure directly
- Use node.serialize() instead of full graph.serialize() in the
promoted widgets test helper used in tight polls
Tests added for each behavior change.
Amp-Thread-ID: https://ampcode.com/threads/T-019e3eb1-878a-77ee-8621-763ba6f1b10b
Co-authored-by: Amp <amp@ampcode.com>
Replace the SUBGRAPH_INPUT_ID sentinel check in buildSlotMetadata with a
check that the upstream node actually resolves via getNodeById. Promoted
widgets (link origin = SUBGRAPH_INPUT sentinel) naturally render as
editable instead of disabled with a phantom upstream chip, and the
renderer no longer imports a litegraph sentinel constant.
Test setup now uses a real upstream node + upstream.connect() instead of
an unreferenced input.link id. Added regression test for the
non-resolvable-upstream case.
Amp-Thread-ID: https://ampcode.com/threads/T-019e3d8c-b6b4-731e-b3fb-9bdb6fefb1ae
Co-authored-by: Amp <amp@ampcode.com>
## Summary
When the built-in logs terminal stayed open during a backend restart,
the buffer froze on pre-restart entries and live log streaming silently
stopped — only closing and reopening the panel resynced. Listen for the
api `reconnected` event and rebuild the terminal contents the same way a
fresh open would.
## Changes
- **What**:
- Extract `useLogsTerminal` composable. The SFC is now a thin shell
holding `terminal: shallowRef<Terminal>` and forwarding to the
composable, so `onMounted`/`onScopeDispose` no longer rely on the
child's emit callback timing.
- Subscribe to `api`'s `reconnected` event via `useEventListener`,
registered synchronously before any awaits. On reconnect:
`terminal.reset()` → refetch raw logs → `scrollToBottom()` →
`subscribeLogs(true)` (the backend loses the per-client subscription on
restart, so re-subscribe is required for live streaming to resume).
- Wrap in-flight resync/mount fetches in AbortControllers. Overlapping
reconnects abort the prior resync, and unmount mid-fetch suppresses
writes to the disposed xterm.
- Hide BaseTerminal whenever `errorMessage` is set so the error layout
doesn't expose an empty xterm container behind the message;
`loading=false` after both load failure and resync success so a later
successful reconnect can clear a stuck spinner.
- Migrate the load/resync error strings to vue-i18n
(`logsTerminal.loadError`, `logsTerminal.resyncError`).
## Review Focus
- **Re-subscribe is the non-obvious half of the fix** — without it, even
after the WebSocket reconnects the backend never resumes streaming logs
to this client because its subscription state was wiped on restart. The
visible "stale buffer" is only one symptom; the silent "no new logs"
symptom needed the explicit `subscribeLogs(true)` re-call in resync.
- `terminal.reset()` lives after a successful raw-logs fetch (not
before) so a failed resync leaves the prior buffer visible instead of
blanking it; resync errors surface via the same inline error message the
mount path uses.
- 8 unit tests around the composable: mount + subscribe, resync ordering
(reset → write → scroll → subscribe via `invocationCallOrder`),
in-flight resync abort on double reconnect, resync error surfacing,
mount-failure-then-recovery, unmount-mid-fetch terminal-write
suppression, listener cleanup on unmount.
- 2 E2E tests using `ws.close()` on the proxied WebSocket as the
reconnect trigger and `subscribeLogs` HTTP fetch count as the sync point
(same pattern as `wsReconnectStaleJob.spec.ts`). Red-checked: disabling
the `reconnected` listener fails exactly the two new tests, all 8
pre-existing tests stay green.
Fixes FE-712
## Screenshots
Before - (After rebooting, the console window does not update from its
state before the reboot must remount the console window for it to
resync.)
https://github.com/user-attachments/assets/b1e49c2c-89a4-4a4a-82b4-064412acee12
After - (The console window syncs automatically after a reboot.)
https://github.com/user-attachments/assets/54b582c5-ad42-41c0-9886-18f4495859da
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12270-fix-terminal-resync-logs-console-on-backend-reconnect-3606d73d3650812fb13fd1934c632344)
by [Unito](https://www.unito.io)
## Summary
Add the first product-area browser coverage on top of the merged typed
route mock foundation: the docked job history sidebar.
## Changes
- **What**: Adds `browser_tests/tests/sidebar/jobHistory.spec.ts` using
`jobsRouteFixture`.
- **What**: Covers direct sidebar entry, docked QPO history entry,
terminal history jobs, active queue jobs, tab filtering, search, clear
queue, and clear history.
- **What**: Adds typed `POST /api/queue` and `POST /api/history` route
helpers that validate request bodies with generated zod schemas.
- **What**: Adds stable test ids for the job history sidebar and queue
progress overlay so tests avoid structural CSS selectors.
- **Dependencies**: Builds on the typed route mock foundation merged in
#12267.
## Review Focus
Review the product assertions and whether this is the right first
coverage slice on top of the typed route mock foundation. This PR
intentionally avoids asset sidebar and floating QPO lifecycle coverage;
those should remain follow-up PRs.
## Screenshots (if applicable)
Not applicable.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12272-test-cover-job-history-sidebar-with-typed-route-mocks-3606d73d3650817481d5f9fac4bfc93c)
by [Unito](https://www.unito.io)