From 1237a34f250f786250c9ceb5231e3cd7d808ca53 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 19 Oct 2026 16:07:06 -0700 Subject: [PATCH] fix(history): name the recorded run's current-file prompts apart from its frozen fields Now that the inspector resolves more saved files, the Recorded run dialog can hold two regions named "Positive prompt" (and two of each Copy/Send action): the frozen run field and the inline current file's prompt. Assistive technology could not tell them apart, and the real-host chronology test read two
 elements.

Prompt blocks take an optional context label. The inline current file inside
a recorded run names its regions "Current file positive prompt" and its
actions "Copy/Send current file positive prompt"; the visible headings stay
as in the concept. The real-host test now also scopes its frozen-field reads
to the "Frozen run prompt texts" section, and a disposable-server workflow
pins the distinct names with a resolved current file.
---
 tests/browser/workspace-history.spec.mjs | 20 ++++++++++++++++++++
 tests/comfyui/execution-history.spec.mjs |  2 +-
 tests/js/prompt-blocks.test.js           | 13 +++++++++++++
 web/js/workspace/file-inspector.mjs      |  4 ++--
 web/js/workspace/prompt-blocks.mjs       | 14 +++++++++-----
 web/js/workspace/recorded-run.mjs        |  2 +-
 6 files changed, 46 insertions(+), 9 deletions(-)

diff --git a/tests/browser/workspace-history.spec.mjs b/tests/browser/workspace-history.spec.mjs
index ee5ac6d..e2b94ac 100644
--- a/tests/browser/workspace-history.spec.mjs
+++ b/tests/browser/workspace-history.spec.mjs
@@ -458,6 +458,26 @@ test('frozen Effective pair uses the shared acknowledged sender and Escape close
     } finally {await host.evaluate(()=>window.historyHost.dispose());await host.close();}
 });
 
+test('the inline current file names its prompts apart from the frozen run fields',async({page})=>{
+    // The current file resolves its own prompts, as a real host's saved PNG does, so the dialog holds two prompt texts per role.
+    await page.route(url=>url.pathname.endsWith('/inspection'),async route=>{const response=await route.fetch();const body=await response.json();
+        body.prompts={positive:{status:'resolved',text:'current file positive',source:'graph-inferred'},negative:{status:'resolved',text:'current file negative',source:'graph-inferred'}};
+        await route.fulfill({response,json:body});});
+    await open(page,page.fixture.detail_id);await marker(page,page.fixture.runs.shared).click();await expect(dialog(page).locator('[data-receipt]')).toHaveCount(5);
+    await dialog(page).getByRole('button',{name:'Raw',exact:true}).click();
+    const frozen=dialog(page).getByRole('region',{name:'Frozen run prompt texts',exact:true});
+    const before=await exact(frozen.getByRole('region',{name:'Positive prompt',exact:true}).locator('pre'));
+    await dialog(page).locator('[data-receipt]').first().getByRole('button',{name:'View current file',exact:true}).click();
+    await expect(page.getByRole('dialog',{name:'Image preview',exact:true})).toBeVisible();await page.keyboard.press('Escape');await expect(dialog(page)).toBeVisible();
+    const current=dialog(page).getByRole('region',{name:'Current reported file',exact:true});await expect(current).toBeVisible();
+    await expect(current.getByRole('region',{name:'Current file positive prompt',exact:true}).locator('pre')).toHaveText('current file positive');
+    await expect(current.getByRole('region',{name:'Current file negative prompt',exact:true}).locator('pre')).toHaveText('current file negative');
+    await expect(current.getByRole('button',{name:'Copy current file positive prompt',exact:true})).toBeVisible();
+    for(const role of ['Positive','Negative'])await expect(dialog(page).getByRole('region',{name:`${role} prompt`,exact:true})).toHaveCount(1);
+    await expect(current.getByRole('region',{name:/^(Positive|Negative) prompt$/})).toHaveCount(0);
+    expect(await exact(dialog(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('pre'))).toBe(before);
+});
+
 test('pagehide tears down dialogs and persisted reentry revalidates before enabling recorded actions',async({page})=>{
     await open(page,page.fixture.detail_id);await marker(page,page.fixture.runs.shared).click();await expect(dialog(page).locator('[data-receipt]')).toHaveCount(5);
     await dialog(page).locator('[data-receipt]').first().getByRole('button',{name:'View current file',exact:true}).click();await expect(page.getByRole('dialog',{name:'Image preview',exact:true})).toBeVisible();
diff --git a/tests/comfyui/execution-history.spec.mjs b/tests/comfyui/execution-history.spec.mjs
index cb85c24..a5332d1 100644
--- a/tests/comfyui/execution-history.spec.mjs
+++ b/tests/comfyui/execution-history.spec.mjs
@@ -243,7 +243,7 @@ test.describe('public chronological history with the actual acknowledged host br
                 const frozen = attempts[0].context;
                 for (const mode of ['Raw', 'Effective']) {
                     await run.getByRole('button', { name: mode, exact: true }).click();
-                    for (const [role, value] of [['Positive', frozen.positive[mode.toLowerCase()]], ['Negative', frozen.negative[mode.toLowerCase()]]]) expect(await run.getByRole('region', { name: `${role} prompt`, exact: true }).locator('pre').evaluate(node => node.textContent)).toBe(value);
+                    for (const [role, value] of [['Positive', frozen.positive[mode.toLowerCase()]], ['Negative', frozen.negative[mode.toLowerCase()]]]) expect(await run.getByRole('region', { name: 'Frozen run prompt texts', exact: true }).getByRole('region', { name: `${role} prompt`, exact: true }).locator('pre').evaluate(node => node.textContent)).toBe(value);
                     await page.evaluate(async id => {const { app } = await import('/scripts/app.js');const node = app.graph.getNodeById(id);node.widgets.find(w => w.name === 'text').value = 'Before send';window.chronologyEffects = { queue: 0, load: 0 };window.chronologyOriginals = {};for (const [method, key] of [['queuePrompt', 'queue'], ['loadGraphData', 'load']]) {const original = app[method];window.chronologyOriginals[method] = original;app[method] = function(...args) {window.chronologyEffects[key]++;return original.apply(this, args);};}}, destination);
                     try {
                         await run.getByRole('button', { name: 'Send prompt pair', exact: true }).click();const sender = workspace.getByRole('dialog', { name: 'Send to ComfyUI', exact: true });
diff --git a/tests/js/prompt-blocks.test.js b/tests/js/prompt-blocks.test.js
index b32c6be..803e04a 100644
--- a/tests/js/prompt-blocks.test.js
+++ b/tests/js/prompt-blocks.test.js
@@ -130,3 +130,16 @@ test('class names in status lines render as text, never markup',()=>{
         assert.equal(f.walk(container,node=>Object.hasOwn(node,'innerHTML')).length,0);
     }finally{scope.dispose();f.restore();}
 });
+
+test('a context label names nested prompt regions and actions apart from the frozen blocks while the visible heading stays',()=>{
+    const f=dom(),scope=createScope(),container=new f.Element('div');
+    try{
+        module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},contextLabel:'Current file',prompts:{positive:record('p'),negative:record('n')}});
+        const labels=f.walk(container,node=>node.attributes['aria-label']).map(node=>node.attributes['aria-label']);
+        for(const name of ['Current file positive prompt','Current file negative prompt','Copy current file positive prompt','Send current file positive prompt','Copy current file negative prompt','Send current file negative prompt']) assert.ok(labels.includes(name),name);
+        assert.equal(labels.filter(name=>/^(Positive|Negative) prompt$/.test(name)).length,0);
+        assert.deepEqual(f.walk(container,node=>node.tagName==='h3').map(node=>node.textContent),['Positive prompt','Negative prompt']);
+        const plain=new f.Element('div');module.mountPromptBlocks(plain,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:record('p')}});
+        assert.ok(f.walk(plain,node=>node.attributes['aria-label']==='Positive prompt').length);
+    }finally{scope.dispose();f.restore();}
+});
diff --git a/web/js/workspace/file-inspector.mjs b/web/js/workspace/file-inspector.mjs
index a6f048f..f52c433 100644
--- a/web/js/workspace/file-inspector.mjs
+++ b/web/js/workspace/file-inspector.mjs
@@ -85,7 +85,7 @@ function formatBytes(value) {
     return value === null || value === undefined ? null : value >= 1048576 ? `${(value / 1048576).toFixed(1)} MB` : `${Math.max(1, Math.round(value / 1024))} KB`;
 }
 
-export function mountFileInspector(container, {sender, parentScope, inline = false, onPreview, onRetry, onRefresh, onInteraction = () => {}}) {
+export function mountFileInspector(container, {sender, parentScope, inline = false, promptContext = '', onPreview, onRetry, onRefresh, onInteraction = () => {}}) {
     if (parentScope.signal.aborted) return {setState() {}, dispose() {}};
     const scope = childScope(parentScope);
     let contentScope;
@@ -145,7 +145,7 @@ export function mountFileInspector(container, {sender, parentScope, inline = fal
         }
         if (chips.childNodes.length) inner.append(chips);
         const prompts = el('div'); inner.append(prompts);
-        const blocks = mountPromptBlocks(prompts, {sender, parentScope:contentScope, includePairAction:false});
+        const blocks = mountPromptBlocks(prompts, {sender, parentScope:contentScope, includePairAction:false, contextLabel:promptContext});
         blocks.setState({prompts:record.prompts}); contentScope.cleanup(blocks.dispose);
         if (library?.status === 'linked') {
             const vocabulary = el('div', undefined, 'file-vocabulary');
diff --git a/web/js/workspace/prompt-blocks.mjs b/web/js/workspace/prompt-blocks.mjs
index f3a3bcb..5c3db6e 100644
--- a/web/js/workspace/prompt-blocks.mjs
+++ b/web/js/workspace/prompt-blocks.mjs
@@ -41,7 +41,11 @@ function childScope(parentScope) {
     return scope;
 }
 
-export function mountPromptBlocks(container, {prompts, sender, parentScope, roles:visibleRoles = ['positive', 'negative'], onRetry, includePairAction = true}) {
+export function mountPromptBlocks(container, {prompts, sender, parentScope, roles:visibleRoles = ['positive', 'negative'], onRetry, includePairAction = true, contextLabel = ''}) {
+    // A nested set of blocks (the current file inside a recorded run) names its regions and actions with a
+    // context so assistive technology never meets two identically named prompt regions in one dialog.
+    const named = role => contextLabel ? `${contextLabel} ${role} prompt` : `${role} prompt`;
+    const action = role => contextLabel ? `${contextLabel[0].toLowerCase()}${contextLabel.slice(1)} ${role} prompt` : `${role} prompt`;
     if (parentScope.signal.aborted) return {update() {}, setState() {}, dispose() {}};
     const scope = childScope(parentScope);
     container.classList.add('prompt-inspector');
@@ -87,7 +91,7 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role
             const title = `${role[0].toUpperCase() + role.slice(1)} prompt`;
             const record = state.prompts?.[role];
             const block = el('section', undefined, `prompt-block prompt-${role}`);
-            block.setAttribute('aria-label', title);
+            block.setAttribute('aria-label', contextLabel ? named(role) : title);
             const heading = el('div', undefined, 'prompt-block-heading');
             heading.append(el('h3', title));
             block.append(heading);
@@ -101,12 +105,12 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role
                     block.append(text);
                 }
                 const actions = el('div', undefined, 'prompt-actions');
-                const send = createSendButton(`Send ${role} prompt`);
+                const send = createSendButton(`Send ${action(role)}`);
                 send.dataset.sendRole = role;
                 send.disabled = !actionsEnabled;
                 const copy = createButton('', 'ca-icon-button');
-                copy.setAttribute('aria-label', `Copy ${role} prompt`);
-                copy.title = `Copy ${role} prompt`;
+                copy.setAttribute('aria-label', `Copy ${action(role)}`);
+                copy.title = `Copy ${action(role)}`;
                 copy.append(createWorkspaceIcon('copy'));
                 copy.dataset.copyRole = role;
                 copy.disabled = !actionsEnabled;
diff --git a/web/js/workspace/recorded-run.mjs b/web/js/workspace/recorded-run.mjs
index 8bfb842..29b815b 100644
--- a/web/js/workspace/recorded-run.mjs
+++ b/web/js/workspace/recorded-run.mjs
@@ -68,7 +68,7 @@ export function mountRecordedRun(container, {api, controller, sender, parentScop
         const receiptNodes = new Map();
         const evidenceValues = new Map();
         const blocks = mountPromptBlocks(prompts, {sender, parentScope:local});
-        const inspector = mountFileInspector(inspectorContainer, {sender, parentScope:local, inline:true, onRetry:() => inspection.retry(), onPreview:() => showViewer(), onRefresh:refreshCurrent});
+        const inspector = mountFileInspector(inspectorContainer, {sender, parentScope:local, inline:true, promptContext:'Current file', onRetry:() => inspection.retry(), onPreview:() => showViewer(), onRefresh:refreshCurrent});
         function selectedReceipt() {
             const selected = latest.selection.receipt;
             return selected && latest.outputs.items.find(item => occurrenceKey(item.occurrence) === occurrenceKey(selected));