diff --git a/tests/test_metadata_collapse.mjs b/tests/test_metadata_collapse.mjs
new file mode 100644
index 0000000..8af3284
--- /dev/null
+++ b/tests/test_metadata_collapse.mjs
@@ -0,0 +1,198 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { imageMetadataMethods } from '../web/resolver/views/tabs_image_metadata_methods.js';
+import { tabsLoadedMethods } from '../web/resolver/views/tabs_loaded_methods.js';
+
+globalThis.window = globalThis;
+
+function createMockContext() {
+ const memory = new Map();
+ const mockStorage = {
+ getItem: (k) => (memory.has(k) ? memory.get(k) : null),
+ setItem: (k, v) => memory.set(k, String(v)),
+ removeItem: (k) => memory.delete(k),
+ };
+ return {
+ ...imageMetadataMethods,
+ ...tabsLoadedMethods,
+ escapeHtml: (s) => String(s ?? ''),
+ escapeJsString: (s) => JSON.stringify(s),
+ getMetadataTooltipBadge: (help, label) => `?`,
+ getCategoryDisplayName: (cat) => cat,
+ getModelToken: (model) => model.name || '',
+ getContextMenuAttrs: () => '',
+ stripModelExtension: (name) => String(name || '').replace(/\.[^/.]+$/, ''),
+ getMissingModelKey: (model) => model.name || 'key',
+ getModelPreviewTooltipAttrs: () => '',
+ bindLoadedModelSelection: () => {},
+ renderStatusMessage: (msg, type = 'info') => `
${msg}
`,
+ storage: mockStorage,
+ };
+}
+
+test('image metadata sections default to expanded', () => {
+ const ctx = createMockContext();
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('tools'), false);
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('summary'), false);
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), false);
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('model-transfer'), false);
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('models'), false);
+});
+
+test('toggling section marks it collapsed and persists state', () => {
+ const ctx = createMockContext();
+ const state = {};
+ ctx.toggleImageInspectorSection('prompt-transfer');
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), true);
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('model-transfer'), false);
+
+ ctx.toggleImageInspectorSection('prompt-transfer');
+ assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), false);
+});
+
+test('renderMetadataPromptTransferPanel renders collapse button with chevron and aria-expanded', () => {
+ const ctx = createMockContext();
+ const state = {
+ promptTransfer: {
+ loading: false,
+ applying: false,
+ error: '',
+ pairs: {},
+ prompts: { source: [], target: [] },
+ },
+ };
+ const expandedHtml = ctx.renderMetadataPromptTransferPanel(state);
+ assert.ok(expandedHtml.includes('data-image-inspector-collapse="prompt-transfer"'));
+ assert.ok(expandedHtml.includes('aria-expanded="true"'));
+ assert.ok(!expandedHtml.includes('mr-image-prompt-transfer is-collapsed'));
+
+ ctx.toggleImageInspectorSection('prompt-transfer');
+ const collapsedHtml = ctx.renderMetadataPromptTransferPanel(state);
+ assert.ok(collapsedHtml.includes('aria-expanded="false"'));
+ assert.ok(collapsedHtml.includes('mr-image-prompt-transfer is-collapsed'));
+});
+
+test('renderMetadataTransferPanel renders collapse button with chevron and aria-expanded', () => {
+ const ctx = createMockContext();
+ const state = {
+ transfer: {
+ loading: false,
+ refreshing: false,
+ applying: false,
+ undoing: false,
+ error: '',
+ targetGroups: [],
+ selectedNodeKeys: new Set(),
+ },
+ };
+ const expandedHtml = ctx.renderMetadataTransferPanel(state);
+ assert.ok(expandedHtml.includes('data-image-inspector-collapse="model-transfer"'));
+ assert.ok(expandedHtml.includes('aria-expanded="true"'));
+ assert.ok(!expandedHtml.includes('mr-image-transfer is-collapsed'));
+
+ ctx.toggleImageInspectorSection('model-transfer');
+ const collapsedHtml = ctx.renderMetadataTransferPanel(state);
+ assert.ok(collapsedHtml.includes('aria-expanded="false"'));
+ assert.ok(collapsedHtml.includes('mr-image-transfer is-collapsed'));
+});
+
+test('displayLoadedModels renders collapse button and tooltip badge, hiding subtitle when collapsible', () => {
+ const ctx = createMockContext();
+ const container = { innerHTML: '', dataset: {} };
+ const data = {
+ total: 1,
+ loaded_models: [
+ { name: 'model.safetensors', category: 'checkpoints', active: true }
+ ],
+ };
+
+ // Non-collapsible (Loaded Models tab) - shows subtitle, no tooltip badge
+ ctx.displayLoadedModels(container, data, { collapsible: false });
+ assert.ok(!container.innerHTML.includes('data-image-inspector-collapse'));
+ assert.ok(container.innerHTML.includes('Loaded Models'));
+ assert.ok(container.innerHTML.includes('class="mr-loaded-models-subtitle"'));
+
+ // Collapsible (Metadata tab) - shows tooltip badge, hides subtitle paragraph
+ ctx.displayLoadedModels(container, data, {
+ collapsible: true,
+ isCollapsed: false,
+ sectionId: 'models',
+ title: 'Referenced Models',
+ tooltip: 'Models reported by embedded Civitai metadata.',
+ });
+ assert.ok(container.innerHTML.includes('data-image-inspector-collapse="models"'));
+ assert.ok(container.innerHTML.includes('aria-expanded="true"'));
+ assert.ok(container.innerHTML.includes('Referenced Models'));
+ assert.ok(container.innerHTML.includes('class="mr-tooltip-badge"'));
+ assert.ok(container.innerHTML.includes('Models reported by embedded Civitai metadata.'));
+ assert.ok(!container.innerHTML.includes('class="mr-loaded-models-subtitle"'));
+
+ // Collapsible and collapsed
+ ctx.displayLoadedModels(container, data, {
+ collapsible: true,
+ isCollapsed: true,
+ sectionId: 'models',
+ title: 'Referenced Models',
+ tooltip: 'Models reported by embedded Civitai metadata.',
+ });
+ assert.ok(container.innerHTML.includes('data-image-inspector-collapse="models"'));
+ assert.ok(container.innerHTML.includes('aria-expanded="false"'));
+});
+
+test('renderImageMetadataResult renders Extracted Metadata with tooltip badge and no subtitle paragraph', () => {
+ const ctx = createMockContext();
+ const resultEl = {
+ innerHTML: '',
+ querySelector: () => ({ innerHTML: '', dataset: {} }),
+ };
+ ctx.contentElement = {
+ querySelector: (sel) => {
+ if (sel === '[data-image-inspector-result]') {
+ return resultEl;
+ }
+ return null;
+ },
+ };
+ ctx.imageInspectorState = {
+ metadata: {
+ filename: 'test.png',
+ image: { width: 512, height: 512, format: 'PNG' },
+ },
+ loadedModels: null,
+ loading: false,
+ error: '',
+ };
+ ctx.renderMetadataPromptTransferPanel = () => '';
+ ctx.renderMetadataTransferPanel = () => '';
+
+ ctx.renderImageMetadataResult();
+ assert.ok(resultEl.innerHTML.includes('Extracted Metadata'));
+ assert.ok(resultEl.innerHTML.includes('data-image-inspector-collapse="summary"'));
+ assert.ok(resultEl.innerHTML.includes('Read-only information from the selected source.'));
+ assert.ok(resultEl.innerHTML.includes('class="mr-tooltip-badge"'));
+ assert.ok(!resultEl.innerHTML.includes('class="mr-loaded-models-subtitle"'));
+});
+
+test('renderImageMetadataResult without metadata renders prompt transfer panel first', () => {
+ const ctx = createMockContext();
+ const resultEl = {
+ innerHTML: '',
+ querySelector: () => null,
+ };
+ ctx.contentElement = {
+ querySelector: (sel) => (sel === '[data-image-inspector-result]' ? resultEl : null),
+ };
+ ctx.imageInspectorState = {
+ metadata: null,
+ loadedModels: null,
+ loading: false,
+ error: '',
+ };
+ ctx.renderMetadataPromptTransferPanel = () => '';
+ ctx.renderMetadataTransferPanel = () => '';
+
+ ctx.renderImageMetadataResult();
+ assert.ok(resultEl.innerHTML.startsWith(' :first-child,
+.mr-image-inspector-result > :first-child,
+#model-resolver-modal .mr-image-inspector-result > .mr-image-prompt-transfer:first-child,
+.mr-image-inspector-result > .mr-image-prompt-transfer:first-child,
+#model-resolver-modal .mr-image-inspector-result > .mr-image-transfer:first-child,
+.mr-image-inspector-result > .mr-image-transfer:first-child {
+ margin-top: 0;
+}
+
.mr-image-inspector-summary {
padding-top: var(--mr-spacing-12);
border-top: 1px solid var(--mr-border);
@@ -4944,9 +5021,7 @@ button.mr-info-link:disabled {
#model-resolver-modal .mr-image-inspector-summary .mr-loaded-models-header,
.mr-image-inspector-summary .mr-loaded-models-header {
- padding-left: 0;
- padding-right: 0;
- padding-top: 0;
+ padding: 0;
border-bottom: 0;
margin-bottom: var(--mr-spacing-8);
}
@@ -4987,9 +5062,9 @@ button.mr-info-link:disabled {
#model-resolver-modal .mr-image-inspector-models .mr-loaded-models-header,
.mr-image-inspector-models .mr-loaded-models-header {
- padding-left: 0;
- padding-right: 0;
- padding-top: 0;
+ padding: 0;
+ border-bottom: 0;
+ align-items: flex-start;
}
#model-resolver-modal .mr-image-inspector-models .mr-models-list,
@@ -5064,6 +5139,94 @@ button.mr-info-link:disabled {
background: linear-gradient(90deg, rgba(114, 211, 138, 0.28), var(--mr-border) 34%, transparent 100%);
}
+#model-resolver-modal .mr-image-inspector.is-tools-collapsed .mr-image-inspector-tools,
+.mr-image-inspector.is-tools-collapsed .mr-image-inspector-tools {
+ display: none;
+}
+
+#model-resolver-modal .mr-image-inspector.is-tools-collapsed .mr-image-inspector-header,
+.mr-image-inspector.is-tools-collapsed .mr-image-inspector-header {
+ padding-bottom: var(--mr-spacing-12);
+}
+
+#model-resolver-modal .mr-image-inspector-summary.is-collapsed .mr-image-inspector-info-table,
+.mr-image-inspector-summary.is-collapsed .mr-image-inspector-info-table {
+ display: none;
+}
+
+#model-resolver-modal .mr-image-inspector-summary.is-collapsed .mr-loaded-models-header,
+.mr-image-inspector-summary.is-collapsed .mr-loaded-models-header {
+ margin-bottom: 0;
+ padding: 0;
+}
+
+#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-list,
+.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-list,
+#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-actions,
+.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-actions,
+#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-error-text,
+.mr-image-prompt-transfer.is-collapsed .mr-error-text {
+ display: none;
+}
+
+#model-resolver-modal .mr-image-prompt-transfer.is-collapsed,
+.mr-image-prompt-transfer.is-collapsed {
+ margin-bottom: var(--mr-spacing-12);
+}
+
+#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-header,
+.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-header {
+ margin-bottom: 0;
+}
+
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-mode,
+.mr-image-transfer.is-collapsed .mr-image-transfer-mode,
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-activity,
+.mr-image-transfer.is-collapsed .mr-image-transfer-activity,
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-category-list,
+.mr-image-transfer.is-collapsed .mr-image-transfer-category-list,
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-actions,
+.mr-image-transfer.is-collapsed .mr-image-transfer-actions,
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-loading,
+.mr-image-transfer.is-collapsed .mr-image-transfer-loading,
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-note,
+.mr-image-transfer.is-collapsed .mr-image-transfer-note,
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-inspector-loading,
+.mr-image-transfer.is-collapsed .mr-image-inspector-loading,
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-error-text,
+.mr-image-transfer.is-collapsed .mr-error-text {
+ display: none;
+}
+
+#model-resolver-modal .mr-image-transfer.is-collapsed,
+.mr-image-transfer.is-collapsed {
+ margin-bottom: var(--mr-spacing-12);
+}
+
+#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-header,
+.mr-image-transfer.is-collapsed .mr-image-transfer-header {
+ margin-bottom: 0;
+}
+
+#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-models-list,
+.mr-image-inspector-models.is-collapsed .mr-models-list,
+#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-models-list-pad,
+.mr-image-inspector-models.is-collapsed .mr-models-list-pad,
+#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-copy-section,
+.mr-image-inspector-models.is-collapsed .mr-copy-section,
+#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-loaded-filter-row,
+.mr-image-inspector-models.is-collapsed .mr-loaded-filter-row {
+ display: none;
+}
+
+#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-loaded-models-header,
+.mr-image-inspector-models.is-collapsed .mr-loaded-models-header {
+ margin-bottom: 0;
+ padding: 0;
+ border-bottom: 0;
+ align-items: flex-start;
+}
+
.mr-image-prompt-header,
.mr-image-prompt-actions {
display: flex;
diff --git a/web/resolver/views/tabs_image_metadata_methods.js b/web/resolver/views/tabs_image_metadata_methods.js
index aaeef09..3372533 100644
--- a/web/resolver/views/tabs_image_metadata_methods.js
+++ b/web/resolver/views/tabs_image_metadata_methods.js
@@ -1,4 +1,5 @@
import { copyTextWithFeedback, safeStorage } from "../utils/html_utils.js";
+import { getSvgIcon } from "../../utils/icon_utils.js";
import {
isExistingResolvedModel,
toResolverContextModel,
@@ -15,6 +16,7 @@ const MAX_IMAGE_FILE_SIZE = 64 * 1024 * 1024;
const MAX_WORKFLOW_FILE_SIZE = 16 * 1024 * 1024;
const METADATA_TRANSFER_MODE_STORAGE_KEY = 'model_resolver_metadata_transfer_mode';
const METADATA_TRANSFER_ACTIVITY_SCOPE_STORAGE_KEY = 'model_resolver_metadata_transfer_activity_scope';
+const METADATA_COLLAPSED_SECTIONS_STORAGE_KEY = 'model_resolver_metadata_collapsed_sections';
const METADATA_PROMPT_ROLES = ['positive', 'negative'];
const MAX_PROMPT_PREVIEW_LENGTH = 12_000;
const COPY_ICON_HTML = '';
@@ -84,6 +86,7 @@ function getImageInspectorState(dialog) {
transfer: null,
promptTransfer: null,
lastTransfer: null,
+ collapsedSections: null,
};
}
return dialog.imageInspectorState;
@@ -184,6 +187,84 @@ export const imageMetadataMethods = {
return Boolean(workflowSignature) && transfer.workflowSignature === workflowSignature;
},
+ getImageInspectorCollapsedSections(state = null) {
+ const rootState = getImageInspectorState(this);
+ const imageState = state || rootState;
+ if (rootState && rootState.collapsedSections instanceof Set) {
+ imageState.collapsedSections = rootState.collapsedSections;
+ return rootState.collapsedSections;
+ }
+ if (!(imageState.collapsedSections instanceof Set)) {
+ let initial = [];
+ const stored = safeStorage.getItem(METADATA_COLLAPSED_SECTIONS_STORAGE_KEY);
+ if (stored) {
+ try {
+ initial = JSON.parse(stored);
+ } catch {
+ initial = [];
+ }
+ }
+ const set = new Set(Array.isArray(initial) ? initial : []);
+ imageState.collapsedSections = set;
+ if (rootState) rootState.collapsedSections = set;
+ }
+ return imageState.collapsedSections;
+ },
+
+ isImageInspectorSectionCollapsed(sectionId, state = null) {
+ if (!sectionId) return false;
+ const collapsed = this.getImageInspectorCollapsedSections(state);
+ return collapsed instanceof Set ? collapsed.has(sectionId) : false;
+ },
+
+ toggleImageInspectorSection(sectionId) {
+ if (!sectionId) return;
+ const state = getImageInspectorState(this);
+ const collapsed = this.getImageInspectorCollapsedSections(state);
+ const nextCollapsed = !collapsed.has(sectionId);
+ if (nextCollapsed) {
+ collapsed.add(sectionId);
+ } else {
+ collapsed.delete(sectionId);
+ }
+ try {
+ safeStorage.setItem(
+ METADATA_COLLAPSED_SECTIONS_STORAGE_KEY,
+ JSON.stringify(Array.from(collapsed)),
+ );
+ } catch {
+ // ignore storage errors
+ }
+ this.applyImageInspectorSectionCollapsed(sectionId, nextCollapsed);
+ },
+
+ applyImageInspectorSectionCollapsed(sectionId, isCollapsed) {
+ const root = this.contentElement?.querySelector('.mr-image-inspector');
+ if (!root) return;
+
+ let container = null;
+ if (sectionId === 'tools') {
+ root.classList.toggle('is-tools-collapsed', isCollapsed);
+ } else if (sectionId === 'summary') {
+ container = root.querySelector('.mr-image-inspector-summary');
+ } else if (sectionId === 'prompt-transfer') {
+ container = root.querySelector('.mr-image-prompt-transfer');
+ } else if (sectionId === 'model-transfer') {
+ container = root.querySelector('.mr-image-transfer');
+ } else if (sectionId === 'models') {
+ container = root.querySelector('.mr-image-inspector-models');
+ }
+
+ if (container) {
+ container.classList.toggle('is-collapsed', isCollapsed);
+ }
+
+ const btn = root.querySelector(`[data-image-inspector-collapse="${sectionId}"]`);
+ if (btn) {
+ btn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
+ }
+ },
+
loadImageMetadata() {
if (!this.contentElement) return null;
this.contentElement.style.overflowY = 'auto';
@@ -198,13 +279,20 @@ export const imageMetadataMethods = {
renderImageMetadataShell() {
if (!this.contentElement) return;
+ const isToolsCollapsed = this.isImageInspectorSectionCollapsed('tools');
const existingRoot = this.contentElement.querySelector('.mr-image-inspector');
if (!existingRoot) {
this.contentElement.innerHTML = `
-