From bfda248db058bb9328ddb17f27612f981f697657 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Tue, 20 Oct 2026 23:49:25 -0700 Subject: [PATCH] feat(history): concept heading and summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Prompt history page takes the approved mock's heading and summary surface (docs/concepts/workspace/app.js renderHistory a591-598, #249 F1): - Heading description is "Prompt history · one positive, one shared gallery"; the actions stay "← Prompt library", "Edit metadata" and button.send "Send to ComfyUI" (aria-label "Send prompt pair to ComfyUI: "), on one row at 1600. Send is disabled only while the selected pair's roles are loading (D24). - The summary is article.surface > span.chip category, the positive and selected negative prompt blocks, then .card-meta "N tracked runs · N images · N negative variants". Tracked runs come from the recorded history window's recorded_run_count (hidden until a read completes, kept while a refresh is in flight); images and variants from the saved positive's image_count and variant_count. - The summary's copy/send actions are named after the prompt, as in the concept ("Copy positive prompt: <title>"); mountPromptBlocks gains setTitle so an in-place metadata edit renames them without a remount. - mountRecordedHistory gains an optional onRecordedCount callback. Selector mapping: - .library-surface (history summary) -> .library-history article.surface - .library-surface .library-chip -> article.surface > span.chip - "Copy|Send <role> prompt[ to ComfyUI]" in "Saved prompt texts" -> "...: <title>" - heading p.muted "One positive prompt. Reusable negative variants. One shared gallery." -> "Prompt history · one positive, one shared gallery" - Unchanged: region "Saved prompt texts", .library-variants, .library-shared-gallery, .recorded-history (still .library-surface). Tests: workspace-history "history heading actions stay on one row at 1600 and are enabled after load" and "summary meta uses recorded runs" (both failed before), a prompt-blocks setTitle unit, and the mapped selectors in workspace-visual, workspace-library-editor and the canonical comparison. --- .../browser/history-canonical-comparison.mjs | 2 +- tests/browser/workspace-history.spec.mjs | 42 +++++++++++++++++++ .../browser/workspace-library-editor.spec.mjs | 2 +- tests/browser/workspace-visual.spec.mjs | 9 ++-- tests/js/prompt-blocks.test.js | 15 +++++++ web/js/workspace/history.mjs | 5 ++- web/js/workspace/library.mjs | 21 ++++++---- web/js/workspace/prompt-blocks.mjs | 14 +++++-- web/js/workspace/workspace.css | 3 ++ 9 files changed, 94 insertions(+), 19 deletions(-) diff --git a/tests/browser/history-canonical-comparison.mjs b/tests/browser/history-canonical-comparison.mjs index 3e5978c..cf98e66 100644 --- a/tests/browser/history-canonical-comparison.mjs +++ b/tests/browser/history-canonical-comparison.mjs @@ -35,7 +35,7 @@ export async function captureCanonicalState(page, testInfo, name, selector) { export async function historyGeometry(page, production) { const selectors = production ? { - surface: '.library-surface', timeline: '.library-branches', positive: '.library-positive-lane', + surface: '.library-history article.surface', timeline: '.library-branches', positive: '.library-positive-lane', branch: '.library-variant-lane', label: '.library-variant-lane span', dot: '.library-variant-dot[aria-pressed="true"]', positiveLabel: '.library-positive-lane span', role: '[aria-label="Saved prompt texts"] .prompt-section', diff --git a/tests/browser/workspace-history.spec.mjs b/tests/browser/workspace-history.spec.mjs index d79183c..9d8d7fd 100644 --- a/tests/browser/workspace-history.spec.mjs +++ b/tests/browser/workspace-history.spec.mjs @@ -721,3 +721,45 @@ test('completed page one stays bounded across mode changes until explicit output await page.keyboard.press('Escape');await retained(); } finally {releaseNext();} }); + +// #249 PR F1: the concept heading (app.js a591–597) and summary surface (a598) with real counts (D24). +const historyHeading=page=>page.locator('#workspace-main > .heading'); +const summary=page=>page.locator('#workspace-main .library-history article.surface'); +test('history heading actions stay on one row at 1600 and are enabled after load',async({page,request})=>{ + await page.setViewportSize({width:1600,height:1100});const f=page.fixture; + let releaseDetail;const held=new Promise(resolve=>{releaseDetail=resolve;}); + await page.route(new RegExp(`/prompt_manager/library/${f.positive_id}\\?`),async route=>{await held;await route.continue().catch(()=>{});}); + try { + await page.goto(`/prompt_manager/workspace#/library/${f.positive_id}`); + const heading=historyHeading(page),send=heading.locator('.heading-actions > button.send'); + await expect(heading.locator('p.muted')).toHaveText('Prompt history · one positive, one shared gallery'); + // D24: disabled only while the selected pair's roles are loading. + await expect(send).toBeDisabled();releaseDetail(); + await expect(recorded(page)).toBeVisible();await expect(send).toBeEnabled(); + } finally {releaseDetail();} + const heading=historyHeading(page),title=await heading.locator('h1').textContent(); + const actions=heading.locator('.heading-actions > button'); + await expect(actions).toHaveCount(3); + expect(await actions.evaluateAll(nodes=>nodes.map(node=>node.textContent.trim()))).toEqual(['← Prompt library','Edit metadata','Send to ComfyUI']); + await expect(actions.nth(2)).toHaveClass('send'); + await expect(actions.nth(2)).toHaveAccessibleName(`Send prompt pair to ComfyUI: ${title}`); + for(const action of [actions.nth(0),actions.nth(1)]) await expect(action).toBeEnabled(); + const boxes=await actions.evaluateAll(nodes=>nodes.map(node=>{const box=node.getBoundingClientRect();return {y:box.y,height:box.height};})); + expect(new Set(boxes.map(box=>box.y)).size).toBe(1); + const row=await heading.locator('.heading-actions').evaluate(node=>node.getBoundingClientRect().height); + expect(row).toBe(boxes[0].height); +}); +test('summary meta uses recorded runs',async({page,request})=>{ + const f=page.fixture;await open(page,f.positive_id); + const detail=await(await request.get(`/prompt_manager/library/${f.positive_id}`)).json(),runs=(await historyRead(request,f.positive_id)).recorded_run_count; + const surface=summary(page),title=await historyHeading(page).locator('h1').textContent(); + await expect(surface).toHaveCount(1); + await expect(surface.locator(':scope > span.chip')).toHaveText(detail.positive.category || 'Uncategorized'); + const plural=(count,word)=>`${count} ${count===1?word:`${word}s`}`; + await expect(surface.locator(':scope > .card-meta > span')).toHaveText([plural(runs,'tracked run'),plural(detail.positive.image_count,'image'),plural(detail.positive.variant_count,'negative variant')]); + // The concept order: category chip, positive block, selected negative block, then the meta line. + expect(await surface.evaluate(node=>[...node.querySelectorAll('.chip, .prompt-section, .card-meta')].filter(item=>item.checkVisibility()).map(item=>item.className))).toEqual(['chip','prompt-section positive','prompt-section negative','card-meta']); + for(const name of ['Copy positive prompt','Send positive prompt to ComfyUI','Copy negative prompt','Send negative prompt to ComfyUI']) + await expect(saved(page).getByRole('button',{name:`${name}: ${title}`,exact:true})).toBeVisible(); + await expect(surface.locator('.prompt-section').first()).toHaveCSS('border-top-width','1px'); +}); diff --git a/tests/browser/workspace-library-editor.spec.mjs b/tests/browser/workspace-library-editor.spec.mjs index d534a35..d29a35f 100644 --- a/tests/browser/workspace-library-editor.spec.mjs +++ b/tests/browser/workspace-library-editor.spec.mjs @@ -565,7 +565,7 @@ test('edit metadata stays on the page and re-reads the selected pair',async({pag await field(dialog,'Title').fill('Edited in place');await field(dialog,'Category').fill('Studio work');await button(dialog,'Save').click();await expect(dialog).toHaveCount(0); await expect(page.locator('#workspace-main h1')).toHaveText('Edited in place');await expect(page.locator('#workspace-main h1')).toBeFocused(); await expect(toast(page)).toHaveText('Saved.'); - await expect(page).toHaveURL(new RegExp(`#/library/${f.positive_id}$`));await expect(historyRegion(page).locator('.library-surface .library-chip').first()).toHaveText('Studio work'); + await expect(page).toHaveURL(new RegExp(`#/library/${f.positive_id}$`));await expect(historyRegion(page).locator('article.surface > span.chip').first()).toHaveText('Studio work'); await expect(pressedVariant(page)).toHaveAccessibleName('Select negative variant 2'); await expect.poll(()=>pairReads.length).toBeGreaterThan(before); await expect(headingActions(page).locator('button.send')).toHaveAccessibleName('Send prompt pair to ComfyUI: Edited in place'); diff --git a/tests/browser/workspace-visual.spec.mjs b/tests/browser/workspace-visual.spec.mjs index 2cb0dc3..44699f4 100644 --- a/tests/browser/workspace-visual.spec.mjs +++ b/tests/browser/workspace-visual.spec.mjs @@ -101,7 +101,8 @@ for (const [width, height] of [[1600, 1100], [390, 844]]) for (const theme of [' const roles = page.getByRole('region', { name: 'Saved prompt texts', exact: true }); const positive = roles.getByRole('region', { name: 'Positive prompt', exact: true }); expect(await positive.evaluate(node => node.nextElementSibling?.getAttribute('aria-label'))).toBe('Negative prompt'); - for (const name of ['Copy positive prompt', 'Send positive prompt to ComfyUI', 'Copy negative prompt', 'Send negative prompt to ComfyUI']) await expect(roles.getByRole('button', { name, exact: true })).toBeVisible(); + const title = await page.locator('#workspace-main > .heading h1').textContent(); + for (const name of ['Copy positive prompt', 'Send positive prompt to ComfyUI', 'Copy negative prompt', 'Send negative prompt to ComfyUI']) await expect(roles.getByRole('button', { name: `${name}: ${title}`, exact: true })).toBeVisible(); // As in the concept, the selected pair's send sits in the history heading, not among the saved texts. await expect(roles.getByRole('button', { name: 'Send prompt pair', exact: true })).toHaveCount(0); await expect(page.locator('#workspace-main > .heading button.send')).toBeEnabled(); @@ -110,7 +111,7 @@ for (const [width, height] of [[1600, 1100], [390, 844]]) for (const theme of [' await expect(page.getByRole('complementary', { name: 'Selected image details', exact: true })).toContainText('canonical-alt.png'); await page.locator('.library-shared-gallery .media-card.selected .image-open').click(); await expect(page.locator('dialog#viewer-dialog')).toBeVisible();await page.keyboard.press('Escape'); - for (const [name, selector] of [['saved', '.library-surface'], ['branches', '.library-variants'], ['gallery', '.library-shared-gallery'], ['chronology', '.recorded-history']]) { + for (const [name, selector] of [['saved', '.library-history article.surface'], ['branches', '.library-variants'], ['gallery', '.library-shared-gallery'], ['chronology', '.recorded-history']]) { await captureCanonicalState(page, testInfo, `production-${name}-${width}-${theme}`, selector); } const run = page.locator(`button[data-run-id="${fixture.canonical_run}"][data-anchor-id="10"]`); @@ -541,7 +542,7 @@ test('Library visual evidence keeps natural cards and returns from real history await noOverflow(page); await page.getByRole('button',{name:'Explore history',exact:true}).first().click(); await expect(page.getByRole('region',{name:'Prompt history',exact:true})).toBeVisible(); - await expect(page.locator('#workspace-main > .heading p.muted')).toHaveText('One positive prompt. Reusable negative variants. One shared gallery.'); + await expect(page.locator('#workspace-main > .heading p.muted')).toHaveText('Prompt history · one positive, one shared gallery'); await page.getByRole('button',{name:'← Prompt library',exact:true}).click(); await expect(page.locator('article[data-prompt-id]')).toHaveCount(6); await expect(page.locator('#workspace-main > .heading p.muted')).toHaveText('One positive. Reusable negatives. Every experiment stays connected.'); @@ -643,7 +644,7 @@ test('embedded history gallery keeps the history heading and disposes its layout for (const layout of ['List','Masonry','Grid']) { await page.getByRole('button',{name:layout,exact:true}).click();await orderedMedia(page); await expect(page.locator('h1')).toHaveText(title); - await expect(page.locator('#workspace-main > .heading p.muted')).toHaveText('One positive prompt. Reusable negative variants. One shared gallery.'); + await expect(page.locator('#workspace-main > .heading p.muted')).toHaveText('Prompt history · one positive, one shared gallery'); } await attachEvidence(page,testInfo,`after-images-embedded-history-${width}`,false); await page.getByRole('button',{name:'← Prompt library',exact:true}).click(); diff --git a/tests/js/prompt-blocks.test.js b/tests/js/prompt-blocks.test.js index 87a6098..89f1a14 100644 --- a/tests/js/prompt-blocks.test.js +++ b/tests/js/prompt-blocks.test.js @@ -152,6 +152,21 @@ test('a title names each action after its prompt (concept "Copy positive prompt: assert.deepEqual(labels,['Positive prompt','Copy positive prompt: Alpine observatory','Send positive prompt to ComfyUI: Alpine observatory']); }finally{scope.dispose();f.restore();} }); +test('setTitle renames the actions in place and keeps the last prompts and state',()=>{ + const f=dom(),scope=createScope(),container=new f.Element('div'); + try{ + const view=module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},title:'Old name',roles:['positive'],prompts:{positive:record('p')}}); + view.setState({prompts:{positive:record('kept')},actionsEnabled:false}); + const labels=()=>f.walk(container,node=>node.attributes['aria-label']).map(node=>node.attributes['aria-label']); + const rendered=container.children[0];view.setTitle('Old name');assert.equal(container.children[0],rendered,'an unchanged title does not re-render'); + view.setTitle('New name'); + assert.deepEqual(labels(),['Positive prompt','Copy positive prompt: New name','Send positive prompt to ComfyUI: New name']); + assert.ok(f.walk(container,node=>node.textContent==='kept').length); + assert.ok(f.walk(container,node=>node.dataset.copyRole==='positive').every(node=>node.disabled===true)); + view.setTitle('');assert.deepEqual(labels(),['Positive prompt','Copy positive prompt','Send positive prompt to ComfyUI']); + scope.dispose();view.setTitle('After dispose'); + }finally{scope.dispose();f.restore();} +}); const shape=node=>({tag:node.tagName,className:node.className,...(node.attributes['aria-label']?{label:node.attributes['aria-label']}:{}), ...(node.children.length?{children:node.children.filter(child=>child.tagName!=='svg').map(shape)}:{text:node.textContent})}); test('prompt block emits concept markup without a source line',()=>{ diff --git a/web/js/workspace/history.mjs b/web/js/workspace/history.mjs index 7015c9f..f8358b5 100644 --- a/web/js/workspace/history.mjs +++ b/web/js/workspace/history.mjs @@ -29,7 +29,7 @@ function place(parent, node, before) { else parent.insertBefore(node, before); } -export function mountRecordedHistory(container, {api, positiveId, parentScope, sender, visit = {}, onParentRevalidate}) { +export function mountRecordedHistory(container, {api, positiveId, parentScope, sender, visit = {}, onParentRevalidate, onRecordedCount}) { const scope = createScope(), dispose = () => scope.dispose(); if (parentScope.signal.aborted) {dispose();return {dispose};} parentScope.signal.addEventListener('abort', dispose, {once:true}); @@ -61,6 +61,9 @@ export function mountRecordedHistory(container, {api, positiveId, parentScope, s const controller = createHistoryController({api, positiveId, restore:visit, onChange:next => { render(next);runView?.setState(next); if (next.parent !== activeParent) {activeParent = next.parent;onParentRevalidate?.(next.parent);} + // The history summary's "N tracked runs" (#249 D24): a completed window read's count; a read in flight keeps the last. + const window = next.window; + if (!window.loading) onRecordedCount?.(window.history_epoch && !window.error ? window.recorded_run_count : null); }}); state = controller.state; runView = mountRecordedRun(container, {api, controller, sender, parentScope:scope, onRestoreFocus:restoreMarker}); diff --git a/web/js/workspace/library.mjs b/web/js/workspace/library.mjs index 20abda1..8ab9df8 100644 --- a/web/js/workspace/library.mjs +++ b/web/js/workspace/library.mjs @@ -416,8 +416,8 @@ export function mountLibrary(main,api,parentScope,{positiveId}={}) { function mountHistory() { const history=el('section',undefined,'library-history');history.setAttribute('aria-label','Prompt history');main.append(history); const body=el('div');history.append(body); - // Concept heading: ← Prompt library, Edit metadata, then the selected pair's Send to ComfyUI. - headingDescription(main,history,'One positive prompt. Reusable negative variants. One shared gallery.'); + // Concept heading (app.js a591–597): ← Prompt library, Edit metadata, then the selected pair's Send to ComfyUI. + headingDescription(main,history,'Prompt history · one positive, one shared gallery'); const actions=headingActions(main,history); const back=createButton('← Prompt library'),edit=createButton('Edit metadata'),send=createButton('','send'); send.append(createWorkspaceIcon('send'),el('span','Send to ComfyUI'));send.disabled=true;actions.append(back,edit,send); @@ -460,8 +460,11 @@ export function mountLibrary(main,api,parentScope,{positiveId}={}) { if(state.error) {const retry=createButton('Retry prompt history');contentScope.listen(retry,'click',()=>loader.load(positiveId));body.append(retry);}return; } detail=state.detail;const content=contentScope; - const summary=el('article',undefined,'library-surface');const categoryChip=el('span','','library-chip');summary.append(categoryChip); + // The concept summary (app.js a598): category chip, positive and selected negative blocks, then the counts (D24). + const summary=el('article',undefined,'surface');const categoryChip=el('span','','chip');summary.append(categoryChip); const promptContainer=el('div');promptContainer.setAttribute('role','region');promptContainer.setAttribute('aria-label','Saved prompt texts');summary.append(promptContainer); + const meta=el('div',undefined,'card-meta'),runsMeta=el('span'),imagesMeta=el('span'),variantsMeta=el('span');runsMeta.hidden=true; + meta.append(runsMeta,imagesMeta,variantsMeta);summary.append(meta); const parentStatus=el('p','','library-parent-status muted');parentStatus.hidden=true;parentStatus.setAttribute('role','status'); const retryParent=createButton('Retry saved Library details'),reloadSaved=createButton('Reload saved Library view');retryParent.hidden=reloadSaved.hidden=true;summary.append(parentStatus,retryParent,reloadSaved); const ownerText=detail.positive.text;let savedReadState='current',parentRequest,parentGeneration=0; @@ -546,9 +549,11 @@ export function mountLibrary(main,api,parentScope,{positiveId}={}) { const archivedCount=Number.isSafeInteger(detail.positive.archived_variant_count)?detail.positive.archived_variant_count:0; showArchived.textContent=`Show archived (${archivedCount})`;showArchived.setAttribute('aria-pressed',String(archivedShown)); }; + const updateMeta=()=>{imagesMeta.textContent=countOf(detail.positive.image_count ?? 0,'image');variantsMeta.textContent=countOf(detail.positive.variant_count ?? 0,'negative variant');}; updateParent=()=>{ - main.querySelector('h1').textContent=displayTitle(detail.positive);categoryChip.textContent=detail.positive.category || 'Uncategorized'; - send.setAttribute('aria-label',`Send prompt pair to ComfyUI: ${displayTitle(detail.positive)}`); + const name=displayTitle(detail.positive); + main.querySelector('h1').textContent=name;categoryChip.textContent=detail.positive.category || 'Uncategorized';updateMeta(); + send.setAttribute('aria-label',`Send prompt pair to ComfyUI: ${name}`);blocks?.setTitle(name); }; let activePairId; updateParent();updateBranches(state); @@ -589,7 +594,7 @@ export function mountLibrary(main,api,parentScope,{positiveId}={}) { if(current.negative_text!==variant.negative_text || current.explicit_empty!==variant.explicit_empty) {changed=true;return variant;} return {...variant,...current}; }); - detail={...detail,positive:fresh.positive,variants};categoryChip.textContent=fresh.positive.category || 'Uncategorized'; + detail={...detail,positive:fresh.positive,variants};categoryChip.textContent=fresh.positive.category || 'Uncategorized';updateMeta(); savedReadState=changed || unobserved || additional?'stale':'current'; parentStatus.textContent=changed?'A saved negative changed identity. This view retains its previous selection and gallery. Reload the saved Library view to inspect current data.':unobserved?(fresh.variants_has_more?'Some loaded saved negatives were not observed in this bounded revalidation page. Reload the saved Library view to validate them.':'Some loaded saved negatives were not present in the revalidation result. This view retains its previous selection and gallery.'):additional?'Saved Library choices changed. This view retains its previous selection and gallery. Reload the saved Library view to inspect current choices.':'Saved Library details revalidated.'; reloadSaved.hidden=savedReadState==='current'; @@ -610,9 +615,9 @@ export function mountLibrary(main,api,parentScope,{positiveId}={}) { if(!historyVisits.has(positiveId)) historyVisits.set(positiveId,{}); mountRecordedHistory(recorded,{api,positiveId,parentScope:content,sender,visit:historyVisits.get(positiveId),onParentRevalidate:parent=>{ if(content.signal.aborted) return;parentGeneration++;parentRequest?.abort();setParentState(parent); - }}); + },onRecordedCount:count=>{runsMeta.hidden=count===null;runsMeta.textContent=count===null?'':countOf(count,'tracked run');}}); // The heading carries the pair send, as in the concept; the saved texts keep their per-role actions. - blocks=mountPromptBlocks(promptContainer,{prompts:libraryPromptRoles(detail),sender,parentScope:content,includePairAction:false}); + blocks=mountPromptBlocks(promptContainer,{prompts:libraryPromptRoles(detail),sender,parentScope:content,includePairAction:false,title:displayTitle(detail.positive)}); function select(pairId,{force=false}={}) { if(savedReadState!=='current' || (!force && galleryScope && activePairId===pairId)) return; if(pairId!==null && !recordFor(pairId)) pairId=null; diff --git a/web/js/workspace/prompt-blocks.mjs b/web/js/workspace/prompt-blocks.mjs index 9b069da..d86d5a0 100644 --- a/web/js/workspace/prompt-blocks.mjs +++ b/web/js/workspace/prompt-blocks.mjs @@ -38,8 +38,8 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role const named = label => contextLabel ? `${contextLabel} ${label.toLowerCase()} prompt` : `${label} prompt`; const action = role => contextLabel ? `${contextLabel[0].toLowerCase()}${contextLabel.slice(1)} ${role} prompt` : `${role} prompt`; // The concept names a role's copy and send after the prompt they act on (app.js a298–302): "…: <title>". - const subject = title ? `: ${title}` : ''; - if (parentScope.signal.aborted) return {update() {}, setState() {}, dispose() {}}; + let subject = title ? `: ${title}` : ''; + if (parentScope.signal.aborted) return {update() {}, setState() {}, setTitle() {}, dispose() {}}; const scope = childScope(parentScope); container.classList.add('prompt-inspector'); scope.listen(container, 'click', async event => { @@ -66,9 +66,10 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role } if (event.target.closest('[data-retry-prompts]')) onRetry?.(); }); - let roles = {}, actionsEnabled = true; + let roles = {}, actionsEnabled = true, current = {prompts}; function render(state) { if (scope.signal.aborted) return; + current = state; roles = {}; actionsEnabled = state.actionsEnabled !== false; container.replaceChildren(); @@ -119,5 +120,10 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role } } render({prompts}); - return {update:prompts => render({prompts}), setState:render, dispose:() => scope.dispose()}; + // A saved title edited in place renames the actions without a remount (the history summary, #249 F1). + const setTitle = next => { + const value = next ? `: ${next}` : ''; + if (value !== subject) {subject = value;render(current);} + }; + return {update:prompts => render({prompts}), setState:render, setTitle, dispose:() => scope.dispose()}; } diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css index 7c6f48e..9eadb1a 100644 --- a/web/js/workspace/workspace.css +++ b/web/js/workspace/workspace.css @@ -214,6 +214,9 @@ pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space .prompt-card .prompt-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; /* D23 */ } /* #249 D23: the prompt blocks' mount point is transparent, so the section sits in `.card-content` as the concept's does. */ .card-content > .prompt-inspector { display: contents; /* D23 */ } +/* #249 D24: the history summary's prompt blocks sit in `article.surface` as the concept's do (app.js a598); the mount + point keeps its "Saved prompt texts" region name. */ +.library-history article.surface > .prompt-inspector { display: contents; /* D24 */ } /* #249 D23: a real title can be long or one unbroken word; it wraps inside the title row (the toolkit's buttons never wrap), while the category chip keeps whole words beside it and wraps only a name wider than its share of the row. */ .card-title button { min-width: 0; white-space: normal; overflow-wrap: anywhere; /* D23 */ }