diff --git a/web/css/linker-main.css b/web/css/linker-main.css
index 470202c..3f65c37 100644
--- a/web/css/linker-main.css
+++ b/web/css/linker-main.css
@@ -273,7 +273,7 @@
--ml-missing-line-strong: rgba(255,255,255,0.13);
--ml-missing-radius: 6px;
display: grid;
- grid-template-columns: minmax(360px, 1.1fr) minmax(320px, 0.9fr);
+ grid-template-columns: minmax(360px, 1fr) 10px var(--ml-missing-detail-track, minmax(320px, 0.9fr));
gap: 0;
height: 100%;
min-height: 0;
@@ -291,9 +291,39 @@
.ml-missing-list-pane {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
- border-right: 1px solid var(--ml-missing-line);
+ border-right: 0;
background: var(--ml-missing-bg);
}
+.ml-missing-browser-splitter {
+ position: relative;
+ min-width: 10px;
+ width: 10px;
+ cursor: col-resize;
+ user-select: none;
+ touch-action: none;
+ background: var(--ml-missing-bg);
+ border-left: 1px solid var(--ml-missing-line);
+ border-right: 1px solid var(--ml-missing-line);
+ outline: none;
+}
+.ml-missing-browser-splitter::before {
+ content: "";
+ position: absolute;
+ top: 16px;
+ bottom: 16px;
+ left: 50%;
+ width: 2px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.13);
+ transform: translateX(-50%);
+ transition: background 0.15s ease, box-shadow 0.15s ease;
+}
+.ml-missing-browser-splitter:hover::before,
+.ml-missing-browser-splitter:focus-visible::before,
+.ml-missing-browser.is-resizing .ml-missing-browser-splitter::before {
+ background: rgba(114,211,138,0.78);
+ box-shadow: 0 0 0 3px rgba(114,211,138,0.12);
+}
.ml-missing-list-toolbar {
display: flex;
align-items: center;
@@ -651,6 +681,9 @@
grid-template-columns: 1fr;
overflow-y: auto;
}
+ .ml-missing-browser-splitter {
+ display: none;
+ }
.ml-missing-list-pane {
max-height: 46vh;
border-right: 0;
diff --git a/web/linker.js b/web/linker.js
index 091f5c6..4fbc35f 100644
--- a/web/linker.js
+++ b/web/linker.js
@@ -39,6 +39,7 @@ class LinkerManagerDialog extends ComfyDialog {
this.pendingDockToSidebar = false;
this.sidebarTabId = "comfyui-model-linker";
this.sidebarOpenModeStorageKey = "model_linker_sidebar_open_mode";
+ this.missingBrowserSplitStorageKey = "model_linker_missing_browser_detail_w";
this.dockButton = null;
this.undockButton = null;
this._floatingRectBeforeDock = null;
@@ -4837,7 +4838,14 @@ class LinkerManagerDialog extends ComfyDialog {
? `${activeCount} downloading`
: (hasAny100Match ? 'Auto-link ready for exact matches' : 'Review matches or search online');
const listLayout = this.getMissingModelsListLayout(missingModels);
- const listStyle = `--ml-missing-model-col:${listLayout.modelPx}px;--ml-missing-type-col:${listLayout.typePx}px;`;
+ let savedDetailWidth = null;
+ try {
+ savedDetailWidth = parseInt(localStorage.getItem(this.missingBrowserSplitStorageKey) || '', 10);
+ } catch (e) {}
+ const splitStyle = Number.isFinite(savedDetailWidth) && savedDetailWidth > 0
+ ? `--ml-missing-detail-track:${savedDetailWidth}px;`
+ : '';
+ const listStyle = `${splitStyle}--ml-missing-model-col:${listLayout.modelPx}px;--ml-missing-type-col:${listLayout.typePx}px;`;
let html = `
@@ -4906,6 +4914,7 @@ class LinkerManagerDialog extends ComfyDialog {
html += `
+
${detailMissing ? this.renderMissingModel(detailMissing, detailIndex) : this.renderStatusMessage('Select a missing model to inspect details.', 'info')}
@@ -4915,6 +4924,8 @@ class LinkerManagerDialog extends ComfyDialog {
}
wireMissingModelsBrowser(container, data, sortedMissingModels) {
+ this.wireMissingBrowserSplitter(container);
+
const selectRow = (row) => {
const key = row.dataset.missingKey;
if (!key || key === this.selectedMissingModelKey) return;
@@ -4953,6 +4964,130 @@ class LinkerManagerDialog extends ComfyDialog {
this.wireMissingModelDetail(container, selectedMissing, selectedIndex);
}
+ wireMissingBrowserSplitter(container) {
+ const browser = container.querySelector('.ml-missing-browser');
+ const splitter = browser?.querySelector('.ml-missing-browser-splitter');
+ if (!(browser instanceof HTMLElement) || !(splitter instanceof HTMLElement)) return;
+
+ this.restoreMissingBrowserSplitWidth(browser);
+
+ splitter.addEventListener('mousedown', (event) => {
+ if (event.button !== 0) return;
+ this.startMissingBrowserSplitDrag(event, browser);
+ });
+
+ splitter.addEventListener('dblclick', () => {
+ browser.style.removeProperty('--ml-missing-detail-track');
+ try {
+ localStorage.removeItem(this.missingBrowserSplitStorageKey);
+ } catch (e) {}
+ });
+
+ splitter.addEventListener('keydown', (event) => {
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
+ event.preventDefault();
+ this.resizeMissingBrowserDetailBy(browser, event.key === 'ArrowLeft' ? 32 : -32);
+ });
+
+ this._missingBrowserResizeObserver?.disconnect?.();
+ if (typeof ResizeObserver === 'function') {
+ this._missingBrowserResizeObserver = new ResizeObserver(() => {
+ if (!this._missingBrowserSplitDragging) {
+ this.restoreMissingBrowserSplitWidth(browser);
+ }
+ });
+ this._missingBrowserResizeObserver.observe(browser);
+ }
+ }
+
+ restoreMissingBrowserSplitWidth(browser) {
+ try {
+ const savedWidth = parseInt(localStorage.getItem(this.missingBrowserSplitStorageKey) || '', 10);
+ if (Number.isFinite(savedWidth) && savedWidth > 0) {
+ this.setMissingBrowserDetailWidth(browser, savedWidth, { persist: false });
+ }
+ } catch (e) {}
+ }
+
+ startMissingBrowserSplitDrag(event, browser) {
+ if (!(browser instanceof HTMLElement)) return;
+
+ const detailPane = browser.querySelector('.ml-missing-detail-pane');
+ if (!(detailPane instanceof HTMLElement)) return;
+
+ event.preventDefault();
+ this._missingBrowserSplitDragging = true;
+ this._missingBrowserSplitStart = {
+ x: event.clientX,
+ width: detailPane.getBoundingClientRect().width
+ };
+ this._missingBrowserSplitBrowser = browser;
+ this._missingBrowserPrevUserSelect = document.body.style.userSelect;
+ this._missingBrowserPrevCursor = document.body.style.cursor;
+ document.body.style.userSelect = 'none';
+ document.body.style.cursor = 'col-resize';
+ browser.classList.add('is-resizing');
+
+ this._onMissingBrowserSplitMove = (moveEvent) => this.onMissingBrowserSplitDrag(moveEvent);
+ this._onMissingBrowserSplitUp = () => this.endMissingBrowserSplitDrag();
+ document.addEventListener('mousemove', this._onMissingBrowserSplitMove);
+ document.addEventListener('mouseup', this._onMissingBrowserSplitUp, { once: true });
+ }
+
+ onMissingBrowserSplitDrag(event) {
+ if (!this._missingBrowserSplitDragging || !this._missingBrowserSplitStart || !this._missingBrowserSplitBrowser) return;
+
+ const nextWidth = this._missingBrowserSplitStart.width - (event.clientX - this._missingBrowserSplitStart.x);
+ this.setMissingBrowserDetailWidth(this._missingBrowserSplitBrowser, nextWidth);
+ }
+
+ endMissingBrowserSplitDrag() {
+ if (!this._missingBrowserSplitDragging) return;
+
+ this._missingBrowserSplitDragging = false;
+ document.removeEventListener('mousemove', this._onMissingBrowserSplitMove);
+ this._missingBrowserSplitBrowser?.classList.remove('is-resizing');
+ this._missingBrowserSplitBrowser = null;
+ this._missingBrowserSplitStart = null;
+ try {
+ document.body.style.userSelect = this._missingBrowserPrevUserSelect || '';
+ document.body.style.cursor = this._missingBrowserPrevCursor || '';
+ } catch (e) {}
+ }
+
+ resizeMissingBrowserDetailBy(browser, delta) {
+ const detailPane = browser.querySelector('.ml-missing-detail-pane');
+ if (!(detailPane instanceof HTMLElement)) return;
+
+ this.setMissingBrowserDetailWidth(browser, detailPane.getBoundingClientRect().width + delta);
+ }
+
+ setMissingBrowserDetailWidth(browser, width, { persist = true } = {}) {
+ const bounds = this.getMissingBrowserSplitBounds(browser);
+ const nextWidth = Math.round(Math.max(bounds.min, Math.min(bounds.max, width)));
+ browser.style.setProperty('--ml-missing-detail-track', `${nextWidth}px`);
+
+ if (persist) {
+ try {
+ localStorage.setItem(this.missingBrowserSplitStorageKey, String(nextWidth));
+ } catch (e) {}
+ }
+ }
+
+ getMissingBrowserSplitBounds(browser) {
+ const browserRect = browser.getBoundingClientRect();
+ const splitter = browser.querySelector('.ml-missing-browser-splitter');
+ const splitterWidth = splitter instanceof HTMLElement
+ ? splitter.getBoundingClientRect().width
+ : 10;
+ const available = Math.max(1, browserRect.width - splitterWidth);
+ const minListWidth = Math.min(360, Math.max(240, Math.floor(available * 0.28)));
+ const max = Math.max(1, available - minListWidth);
+ const min = Math.min(max, Math.min(420, Math.max(300, Math.floor(available * 0.3))));
+
+ return { min, max };
+ }
+
wireMissingModelDetail(container, missing, missingIndex) {
this.wireLocalMatchButtons(container, missing, missingIndex);
this.wireDownloadSearchPanel(container, missing);