`;
+ const metrics=[['Requests in progress',data.failed?'–':active.length,data.failed?'Not available yet':active[0]?label(active[0].state):'No active requests',first?conversationLink(first.thread):path(project.id,'conversations'),!!first],['Needs your input',data.failed?'–':waiting.length,data.failed?'Not available yet':waiting.length?label(waiting[0].state):'Nothing waiting',waiting[0]?conversationLink(waiting[0].thread):path(project.id,'conversations'),!!waiting[0]],['Conversations',threads.length,'Project conversations',path(project.id,'conversations')],['Files',count,filesKnown?`${f.reviewed.length} reviewed · ${f.drafts.length} draft · ${f.docs.length} docs`:data.recordsMissing?'No project records yet':'Not available yet',path(project.id,'files')]];
+ return `${waiting.length?`
${waiting.length} ${waiting.length===1?'decision needs':'decisions need'} your input. Review decision
`;
}
function tasksView(data) {
const refs=taskRefs(data);
diff --git a/packages/webui/tests/assistant-threads-browser.test.mjs b/packages/webui/tests/assistant-threads-browser.test.mjs
index 79f2ffe39..e265c43d6 100644
--- a/packages/webui/tests/assistant-threads-browser.test.mjs
+++ b/packages/webui/tests/assistant-threads-browser.test.mjs
@@ -358,6 +358,9 @@ test('SP7: each pane links its thread\'s full view, and one plain click reaches
await wait(`${ready(`alpha:${T.a1}`)} && ${ready(`beta:${T.b1}`)}`);
assert.deepEqual(await b.evaluate(`[...document.querySelectorAll('[data-assist-pane]')].map(p => { const a = p.querySelector('.assist-pane-h a[data-assist-pane-full]'); return a && [a.getAttribute('href'), a.textContent.trim(), a.hasAttribute('data-full-view')]; })`),
[[`#/orgs/alpha/projects/threads/${T.a1}`, 'Full view', true], [`#/orgs/beta/projects/threads/${T.b1}`, 'Full view', true]]);
+ // Each pane's Full view names its thread and organization, so a screen reader can tell three apart.
+ assert.deepEqual(await b.evaluate(`[...document.querySelectorAll('[data-assist-pane-full]')].map(a => a.getAttribute('aria-label'))`),
+ ['Full view of Launch email, Alpha', 'Full view of Gateway auth, Beta']);
await b.evaluate(`${PANE(`beta:${T.b1}`)}.querySelector('a[data-assist-pane-full]').click()`);
await wait(`!document.querySelector('[data-assistant]') && /Beta tokens rotate nightly/.test(document.getElementById('bus-view').innerText)`);
assert.equal(await b.evaluate('location.hash'), `#/orgs/beta/projects/threads/${T.b1}`);
@@ -385,11 +388,14 @@ const headerRows = `[...document.querySelectorAll('[data-assist-pane]')].map(p =
const inside = [...p.querySelectorAll('.assist-pane-h > *, .assist-pane-h a, .assist-pane-h button')].every(e => { const q = e.getBoundingClientRect(); return q.left >= h.left - 1 && q.right <= h.right + 1; });
return { oneRow: Math.max(...row) - Math.min(...row) <= 4, chipsBelow: r('[data-assist-chips]').top >= r('[data-assist-title]').bottom - 1, inside };
})`;
-for (const [width, height, keys] of [[1440, 900, [`alpha:${T.a1}`, `alpha:${T.a2}`, `beta:${T.b1}`]], [390, 844, [`beta:${T.b1}`]]]) {
- test(`SP7: a pane header keeps its title and tools on one row with the chips below, at ${width}`, async t => {
+// At 800 three panes are too narrow for one row: the tools wrap, and none of them leaves the pane.
+const THREE = [`alpha:${T.a1}`, `alpha:${T.a2}`, `beta:${T.b1}`];
+for (const [width, height, keys, oneRow] of [[1440, 900, THREE, true], [800, 900, THREE, null], [761, 900, THREE, null], [390, 844, [`beta:${T.b1}`], true]]) {
+ test(`SP7: a pane header keeps its tools inside the pane${oneRow ? ', title and tools on one row with the chips below' : ''}, at ${width}`, async t => {
const { b, wait } = await open(t, { width, height, hash: `#/assistant?open=${keys.join(',')}` });
await wait(keys.map(k => ready(k)).join(' && '));
await wait(`[...document.querySelectorAll('[data-assist-pane] [data-ws-orgchip]')].length === ${keys.length}`);
- assert.deepEqual(await b.evaluate(headerRows), keys.map(() => ({ oneRow: true, chipsBelow: true, inside: true })));
+ const rows = await b.evaluate(headerRows);
+ assert.deepEqual(rows.map(r => (oneRow ? r : { ...r, oneRow: null })), keys.map(() => ({ oneRow, chipsBelow: true, inside: true })));
});
}
diff --git a/packages/webui/tests/projects-review-browser.test.mjs b/packages/webui/tests/projects-review-browser.test.mjs
index c97f7da87..485e8414c 100644
--- a/packages/webui/tests/projects-review-browser.test.mjs
+++ b/packages/webui/tests/projects-review-browser.test.mjs
@@ -168,3 +168,6 @@ test('S4 fix stale project gather cannot enrich a newer request revision', async
// SP7 review: the requirement view sends the person to the conversation's Request details, which a pane
// does not have, so both of its "Open conversation" links are marked as the full view.
test('SP7 requirement view links the conversation\'s full view, not a pane',async t=>{const {b,wait}=await setup(t,'/projects/'+P+'/requirements');await wait(`document.querySelector('[data-prd]')`);const links=await b.evaluate(`[...document.querySelectorAll('[data-project-content] a')].filter(a=>a.textContent.trim()==='Open conversation').map(a=>a.hasAttribute('data-full-view'))`);assert.deepEqual(links,[true,true]);});
+// SP7 re-check: the overview's "Review decision" and its two request counts lead to a request, which only
+// the conversation's full view shows, so each link that names a conversation is marked as the full view.
+test('SP7 overview decision and request links go to the conversation\'s full view',async t=>{const {b,wait}=await setup(t,'/projects');await b.evaluate(`reviewStates['request-1'].wait={kind:'decision'};document.querySelector('#refresh').click();location.hash='#/projects/${P}'`);await wait(`document.querySelector('.project-decision a')`);const got=await b.evaluate(`({decision:[...document.querySelectorAll('.project-decision a')].map(a=>[a.textContent.trim(),a.hasAttribute('data-full-view'),/\\/threads\\//.test(a.getAttribute('href'))]),metrics:[...document.querySelectorAll('.project-metrics a')].map(a=>[a.querySelector('span').textContent,/\\/threads\\//.test(a.getAttribute('href')),a.hasAttribute('data-full-view')])})`);assert.deepEqual(got,{decision:[['Review decision',true,true]],metrics:[['Requests in progress',true,true],['Needs your input',true,true],['Conversations',false,false],['Files',false,false]]});});