diff --git a/packages/webui/src/public/assistant.js b/packages/webui/src/public/assistant.js index 9f7db0540..f361f3cfb 100644 --- a/packages/webui/src/public/assistant.js +++ b/packages/webui/src/public/assistant.js @@ -132,6 +132,7 @@

Reading…

+ Full view Messages
@@ -344,7 +345,9 @@ document.addEventListener('click', e => { if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const a = e.target.closest?.('a[href]'); - if (!a || a.hasAttribute('download') || (a.target && a.target !== '_self')) return; + // A link marked data-full-view (a pane's Full view, a requirement's "Open conversation") goes to the + // organization's own thread view, which has Request details, sessions and sharing. + if (!a || a.hasAttribute('download') || a.hasAttribute('data-full-view') || (a.target && a.target !== '_self')) return; const m = THREAD_LINK.exec(a.getAttribute('href') || ''); const org = m && (m[1] || window.MosaicOrg?.id); if (!m || !org || !ORG.test(org)) return; diff --git a/packages/webui/src/public/projects.js b/packages/webui/src/public/projects.js index 9861b1be8..d694f6e37 100644 --- a/packages/webui/src/public/projects.js +++ b/packages/webui/src/public/projects.js @@ -209,7 +209,7 @@ const candidate=d?.candidate, expanded=r && expandedSet.has(project.id+':'+r.id); const docPath=candidate?.prd_path, docLink=project.repo && typeof docPath==='string' && /^docs\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(docPath) ? `#/docs/${encodeURIComponent(project.repo)}/${encodeURIComponent(docPath)}` : null; const rows=candidate ? [['PRD version',candidate.prd_version],['Approved snapshot',d.snapshot.id || 'None'],['PRD file',docPath],['PRD blob',candidate.prd_blob],['Records commit',candidate.records_commit],['Confirmed intent commit',candidate.spec_commit],['Mode revision',candidate.mode_revision]].map(([label,value])=>`
${label}${label==='PRD file' && docLink?`${clean(value)}`:`${clean(value)}`}
`).join('') : '

Unavailable until a PRD is read.

'; - return `${r?`
${select || `Request ${clean(r.id)}`} · ${chip(r.state)} · from ${clean(r.thread.name)} Open conversation
`:select}

Project direction

North star

${north!==null?`
Open north star in Docs`:data.northRecordsMissing?`

${window.MosaicRecords.markup(project.repo)}

`:data.northMissing?'

No north star yet.

':project.repo?'

North star unavailable. Docs could not read this project’s repository.

':`

This project has no linked repository. The north star and project documents live in a repository. Linking one to an existing project isn't available yet.

`}

${candidate?`PRD ${clean(candidate.prd_version)}`:'PRD'} ${candidate?`${d.snapshot.id?'Approved':'Draft'}`:''}

${d?`

${d.snapshot.id?'Approved snapshot '+clean(d.snapshot.id):'This candidate has no active approved snapshot.'}

Requirements ${d.requirements.length}

${d.requirements.map((req,i)=>`
${clean(req.id)} · ${clean(req.title)}

${clean(req.text)}

Requirement identity: ${clean(candidate.requirements[i]?.path)} · ${clean(candidate.requirements[i]?.blob)}

`).join('')}
`:`

${(data.failed || data.requests.some(x=>x.materialFailed))?'Request or PRD unavailable. Refresh to try again.':'No current PRD candidate or approved snapshot. Waiting for a PRD submission.'}

`}${d && data.requests.some(x=>x.materialFailed)?'

Some request PRDs could not be read. Refresh to try again.

':''}
`; + return `${r?`
${select || `Request ${clean(r.id)}`} · ${chip(r.state)} · from ${clean(r.thread.name)} Open conversation
`:select}

Project direction

North star

${north!==null?`
Open north star in Docs`:data.northRecordsMissing?`

${window.MosaicRecords.markup(project.repo)}

`:data.northMissing?'

No north star yet.

':project.repo?'

North star unavailable. Docs could not read this project’s repository.

':`

This project has no linked repository. The north star and project documents live in a repository. Linking one to an existing project isn't available yet.

`}

${candidate?`PRD ${clean(candidate.prd_version)}`:'PRD'} ${candidate?`${d.snapshot.id?'Approved':'Draft'}`:''}

${d?`

${d.snapshot.id?'Approved snapshot '+clean(d.snapshot.id):'This candidate has no active approved snapshot.'}

Requirements ${d.requirements.length}

${d.requirements.map((req,i)=>`
${clean(req.id)} · ${clean(req.title)}

${clean(req.text)}

Requirement identity: ${clean(candidate.requirements[i]?.path)} · ${clean(candidate.requirements[i]?.blob)}

`).join('')}
`:`

${(data.failed || data.requests.some(x=>x.materialFailed))?'Request or PRD unavailable. Refresh to try again.':'No current PRD candidate or approved snapshot. Waiting for a PRD submission.'}

`}${d && data.requests.some(x=>x.materialFailed)?'

Some request PRDs could not be read. Refresh to try again.

':''}
`; } function overview(data,project,canSend,receipt='') { const active=data.requests.filter(r=>r.state && !ended(r.state)), waiting=active.filter(r=>needs(r.state)), ordered=[...data.requests].sort(requestOrder), first=ordered[0], threads=[...data.threads]; diff --git a/packages/webui/tests/assistant-threads-browser.test.mjs b/packages/webui/tests/assistant-threads-browser.test.mjs index f9fd6c7e5..b2de14575 100644 --- a/packages/webui/tests/assistant-threads-browser.test.mjs +++ b/packages/webui/tests/assistant-threads-browser.test.mjs @@ -350,3 +350,27 @@ test('SP7: each pane names its own organization\'s Messages page, and the bar no [['#/orgs/alpha/messages', 'Messages in Alpha', 'Messages'], ['#/orgs/beta/messages', 'Messages in Beta', 'Messages']]); assert.doesNotMatch(await b.evaluate(`document.querySelector('.assist-bar').innerText`), /Messages/); }); + +// SP7 review: a pane has no Request details, sessions or sharing, so each pane links its thread's full +// view, and a link marked data-full-view is not turned into a pane. +test('SP7: each pane links its thread\'s full view, and one plain click reaches it', async t => { + const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}` }); + 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]]); + 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}`); +}); + +test('SP7: a link marked as the full view stays in the organization\'s own view, from any page', async t => { + const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1}` }); + await wait(ready(`alpha:${T.a1}`)); + // On the Assistant page itself: no pane is added, the browser follows the link. + await b.evaluate(`(() => { const a = document.createElement('a'); a.href = '#/orgs/beta/projects/threads/${T.b1}'; a.setAttribute('data-full-view', ''); a.textContent = 'thread'; document.body.append(a); a.click(); a.remove(); })()`); + await wait(`location.hash === '#/orgs/beta/projects/threads/${T.b1}'`); + await wait(`!document.querySelector('[data-assistant]') && /Beta tokens rotate nightly/.test(document.getElementById('bus-view').innerText)`); + // From another page: the same. + await b.evaluate(`(() => { const a = document.createElement('a'); a.href = '#/orgs/alpha/projects/threads/${T.a1}'; a.setAttribute('data-full-view', ''); a.textContent = 'thread'; document.body.append(a); a.click(); a.remove(); })()`); + await wait(`location.hash === '#/orgs/alpha/projects/threads/${T.a1}' && !document.querySelector('[data-assistant]')`); +}); diff --git a/packages/webui/tests/projects-review-browser.test.mjs b/packages/webui/tests/projects-review-browser.test.mjs index 032aacfc5..c97f7da87 100644 --- a/packages/webui/tests/projects-review-browser.test.mjs +++ b/packages/webui/tests/projects-review-browser.test.mjs @@ -165,3 +165,6 @@ test('S4 fix stale project gather cannot enrich a newer request revision', async const x=await b.evaluate(`({revision:document.querySelector('.project-request-intro').dataset.requestRevision,prd:document.querySelector('[data-work-prd]').textContent,files:document.querySelector('[data-work-files]').textContent})`); assert.equal(x.revision,'2');assert.doesNotMatch(x.prd,/v3|Approved/);assert.doesNotMatch(x.files,/4 draft/); }); +// 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]);}); diff --git a/packages/webui/tests/workspace-pages-browser.test.mjs b/packages/webui/tests/workspace-pages-browser.test.mjs index 2ff4b252d..97bb11903 100644 --- a/packages/webui/tests/workspace-pages-browser.test.mjs +++ b/packages/webui/tests/workspace-pages-browser.test.mjs @@ -188,7 +188,7 @@ test('Inbox lists both organizations, copies a command with --org and the full i assert.deepEqual(items.map(([k]) => k).sort(), [`acme:${ACME_DECISION}`, `beta:${BETA_DECISION}`]); // Each row names its organization, project and session. const acme = items.find(([k]) => k.startsWith('acme'))[1]; - assert.match(acme, /Acme Studio/); assert.match(acme, /Website/); assert.match(acme, /coder/); assert.match(acme, /gated/); + assert.match(acme, /Acme Studio/); assert.match(acme, /Website/); assert.match(acme, /coder/); assert.match(acme, /Gated/); const beta = items.find(([k]) => k.startsWith('beta'))[1]; assert.match(beta, /Beta Works/); assert.match(beta, /pm/); // SP3: a project chip only where the item has a project.