From ab4c7cc96d1d8bf52e3313981203092b560e1c7b Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Sun, 18 Oct 2026 05:04:45 -0700 Subject: [PATCH] fix(inspector): preserve fact states and touch targets --- tests/browser/full-inspector.spec.mjs | 45 +++++++++++++++++++++++++++ web/js/workspace/file-inspector.mjs | 12 +++++-- web/js/workspace/inspector.css | 5 +++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/browser/full-inspector.spec.mjs b/tests/browser/full-inspector.spec.mjs index 09f66cfd..db44f200 100644 --- a/tests/browser/full-inspector.spec.mjs +++ b/tests/browser/full-inspector.spec.mjs @@ -98,6 +98,10 @@ test('full inspector shows six facts and scoped provenance',async({page,request} await expect(panel.locator('.file-evidence')).toHaveText('Workflow prompt evidence'); expect(await panel.evaluate(node=>node.getBoundingClientRect().width)).toBe(310); expect(await panel.locator('.file-inspector-inner').evaluate(node=>getComputedStyle(node).padding)).toBe('17px'); + if (!await page.evaluate(()=>matchMedia('(pointer: coarse)').matches)) { + const copy=await panel.getByRole('button',{name:'Copy positive prompt',exact:true}).boundingBox();expect(copy.width).toBe(30);expect(copy.height).toBe(26); + expect((await panel.getByRole('button',{name:'Send prompt pair',exact:true}).boundingBox()).height).toBe(30); + } await expect(panel.locator('.generation-facts dt')).toHaveText(['Seed','Steps','CFG','Sampler','Scheduler','Model']); await expect(panel.locator('.generation-facts dd')).toHaveText(['9007199254740993','28','6.5','dpmpp_2m','karras','fixture-model.safetensors']); await expect(panel).toContainText('Recipe tags');await expect(panel).toContainText('Recipe category'); @@ -189,3 +193,44 @@ test('full inspector labels container representations and releases action URLs', await page.evaluate(()=>{window.renderFixture.view.dispose();window.renderFixture.view.dispose();window.renderFixture.scope.dispose();});await expect(panel).toBeEmpty(); expect(await page.evaluate(()=>window.renderFixture.revoked)).toEqual(await page.evaluate(()=>window.renderFixture.created)); }); + +test('full inspector distinguishes missing, limited and legacy unknown generation facts',async({page,request})=>{ + await fullFixture(page,request);const panel=inspector(page); + await selectFile(page,'empty.png');await expect(panel.locator('.generation-facts dd')).toHaveText(Array(6).fill('Not recorded')); + await page.locator('article[data-image-key="0:architecture/study-01.png"] .image-open').click();await expect(panel.locator('.generation-facts dd')).toHaveText(Array(6).fill('Not recorded')); + await selectFile(page,'control-heavy.png'); + const explanation='Configuration exceeds inspection limits; download the original file to inspect it locally.'; + await expect(panel.getByText(explanation,{exact:true})).toHaveCount(1);await expect(panel.getByText(explanation,{exact:true})).toBeVisible(); + for(const value of await panel.locator('.generation-facts dd').all())await expect(value).toHaveAccessibleDescription(explanation); + await page.evaluate(async()=>{ + const {mountFileInspector}=await import('/prompt_manager/js/workspace/file-inspector.mjs'); + const {toInspectionRecord}=await import('/prompt_manager/js/workspace/inspection-state.mjs'); + const {createScope}=await import('/prompt_manager/js/shared/toolkit/core.js'); + const container=document.createElement('section');container.id='legacy-facts';document.body.append(container); + window.legacyFactsScope=createScope();window.legacyFactsView=mountFileInspector(container,{parentScope:window.legacyFactsScope}); + window.legacyFactsView.setState({record:toInspectionRecord({prompts:{}},{root_index:0,path:'legacy.png'})}); + }); + const legacy=page.locator('#legacy-facts');await expect(legacy.locator('.generation-facts dd')).toHaveText(Array(6).fill('Unresolved'));await expect(legacy).not.toContainText(explanation);await expect(legacy.locator('.generation-facts dd[aria-describedby]')).toHaveCount(0); + await page.evaluate(()=>{window.legacyFactsView.dispose();window.legacyFactsScope.dispose();}); +}); + +test.describe('full inspector coarse pointer',()=>{ + test.use({hasTouch:true,viewport:{width:390,height:844}}); + test('full inspector meets coarse target sizes and keeps long content reachable at zoom',async({page,request})=>{ + await fullFixture(page,request);expect(await page.evaluate(()=>matchMedia('(pointer: coarse)').matches)).toBe(true);const panel=inspector(page); + await expect(panel.locator('.prompt-positive pre')).toContainText('Full prompt context, fine geometry and light.'); + const fullText=await panel.locator('.prompt-positive pre').textContent();expect(fullText.length).toBeGreaterThan(1000); + await panel.getByText('File & provenance',{exact:true}).click();await panel.getByText('Workflow & raw metadata',{exact:true}).click(); + const bounds=await panel.locator('button,a,summary').evaluateAll(nodes=>nodes.map(node=>({name:node.getAttribute('aria-label')||node.textContent,role:node.matches('.prompt-actions button'),width:node.getBoundingClientRect().width,height:node.getBoundingClientRect().height}))); + for(const target of bounds){expect(target.width,target.name).toBeGreaterThanOrEqual(target.role?36:40);expect(target.height,target.name).toBeGreaterThanOrEqual(target.role?36:40);} + for(const zoom of [1,2]) { + await page.evaluate(zoom=>{document.documentElement.style.zoom=String(zoom);},zoom); + await expect.poll(()=>page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true); + await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); + for(const target of [panel.locator('summary').first(),panel.getByRole('button',{name:'Download metadata JSON',exact:true}),panel.getByRole('link',{name:'Prompt history',exact:true})]) { + await target.scrollIntoViewIfNeeded();await target.focus();await expect(target).toBeFocused();await expect(target).toBeInViewport(); + } + await expect(panel.locator('.prompt-positive pre')).toHaveText(fullText); + } + }); +}); diff --git a/web/js/workspace/file-inspector.mjs b/web/js/workspace/file-inspector.mjs index 7128b4b6..2a7d304c 100644 --- a/web/js/workspace/file-inspector.mjs +++ b/web/js/workspace/file-inspector.mjs @@ -74,7 +74,7 @@ function definitionList(entries, className) { function factText(fact) { if (fact?.status === 'known' && typeof fact.value === 'string') return fact.value; if (fact?.status === 'ambiguous') return 'Ambiguous'; - if (fact?.status === 'absent') return 'Not recorded'; + if (fact?.status === 'not-recorded') return 'Not recorded'; return fact?.reason === 'projection-limited' ? 'Unavailable within inspection limits' : 'Unresolved'; } function formatBytes(value) { @@ -150,6 +150,14 @@ export function mountFileInspector(container, {sender, parentScope, inline = fal } else if (!library) inner.append(el('p', 'Saved vocabulary is unavailable for this inspection response.', 'muted')); const facts = definitionList(FACTS.map(([key, label]) => [label, factText(record.generation[key])]), 'generation-facts'); inner.append(facts); + if (FACTS.some(([key]) => record.generation[key]?.reason === 'projection-limited')) { + const explanation = el('p', 'Configuration exceeds inspection limits; download the original file to inspect it locally.', 'generation-limit muted'); + explanation.id = `file-fact-limit-${++factReasonId}`; + FACTS.forEach(([key], index) => { + if (record.generation[key]?.reason === 'projection-limited') facts.children[index].querySelector('dd').setAttribute('aria-describedby', explanation.id); + }); + inner.append(explanation); + } const provenance = disclosure('File & provenance'); provenance.append(el('p', 'Current file at this source; historical bytes are not verified.', 'muted')); if (file) provenance.append(definitionList([ @@ -217,7 +225,7 @@ export function mountFileInspector(container, {sender, parentScope, inline = fal } return {setState, dispose:() => scope.dispose()}; } -let downloadReasonId = 0; +let downloadReasonId = 0, factReasonId = 0; /** Compatible standalone controller; Images owns a single shared controller instead. */ export function mountPromptInspector(container, {api, image, sender, parentScope}) { diff --git a/web/js/workspace/inspector.css b/web/js/workspace/inspector.css index 959b4640..dac5a7ba 100644 --- a/web/js/workspace/inspector.css +++ b/web/js/workspace/inspector.css @@ -17,6 +17,7 @@ .file-vocabulary > p { margin-block: 0 6px; font-size: 10px; } .file-vocabulary .file-chip { font-size: 12px; } .generation-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-block: 16px; font-size: 11px; } +.generation-limit { font-size: 10px; line-height: 1.65; margin-block: 12px; } .generation-facts dt, .file-facts dt { color: var(--ca-muted); font-size: 10px; } .generation-facts dd, .file-facts dd { margin: 3px 0 0; } .file-inspector details { border-block-start: 1px solid var(--ca-line); margin-block-start: 16px; padding-block-start: 13px; font-size: 12px; } @@ -36,3 +37,7 @@ .file-inspector-footer .workspace-icon { inline-size: 14px; block-size: 14px; } .file-status { padding: 17px; } @media (max-width: 700px) { .ca-tools .file-cover { block-size: 200px; } } +@media (pointer: coarse) { + .ca-tools .file-inspector :is(button, a, summary) { min-inline-size: 40px; min-block-size: 40px; } + .ca-tools .file-inspector .prompt-actions button { inline-size: 36px; block-size: 36px; min-inline-size: 36px; min-block-size: 36px; } +}