diff --git a/packages/webui/src/public/workspace.js b/packages/webui/src/public/workspace.js
index 257b7684e..44fac0663 100644
--- a/packages/webui/src/public/workspace.js
+++ b/packages/webui/src/public/workspace.js
@@ -179,7 +179,7 @@
const secH = (title, note = '', link = '', attrs = '') => `
@@ -1143,10 +1143,10 @@
// page for it: once the page's own reads have answered (`opts.page`), a drawer whose instances read
// has not says its state is being read, and repaints when the read answers. That answer is kept for
// the one repaint it asks for; a repaint while the read is still open waits on the same read.
- let drawerInst = null; // { org, done, value, p }
+ let drawerInst = null, drawerReads = 0; // { org, n, done, value, p }
const drawerInstRead = org => {
if (drawerInst?.org === org) return drawerInst;
- const r = { org, done: false, value: null };
+ const r = { org, n: ++drawerReads, done: false, value: null };
r.p = data.instances(org).then(v => ({ inst: instancesOf(v), copies: copiesOf(v) }), err => ({ inst: null, copies: {}, err }))
.then(v => { r.done = true; r.value = v; return v; });
drawerInst = r;
@@ -1166,12 +1166,12 @@
await Promise.race([ir.p, opts.page ?? ir.p]);
const pending = !ir.done;
// SP14 (#1873): when the read answers, only the drawer repaints, from the reads it already has; the
- // page's own reads are not made again. A drawer closed before the read answered, or one whose read
- // a later open replaced, repaints nothing, and its answer is not kept for a later open.
+ // page's own reads are not made again. Each open repaints its own drawer if it is still on screen and
+ // still waits on this read (data-drawer-pending names the read), so a second agent opened while the
+ // read was open repaints too. A closed drawer repaints nothing, and the answer is not kept for a later open.
if (pending) ir.p.then(() => {
- const mine = drawerInst === ir;
- if (mine) drawerInst = null;
- const node = mine ? document.querySelector(`#bus-view [data-ws-drawer="${CSS.escape(id)}"][data-drawer-pending]`) : null;
+ if (drawerInst === ir) drawerInst = null;
+ const node = document.querySelector(`#bus-view [data-ws-drawer="${CSS.escape(id)}"][data-drawer-pending="${ir.n}"]`);
if (!node) return;
const key = focusKey(node), tpl = document.createElement('template');
tpl.innerHTML = drawerHtml(pick, { body, business, config, pending: false, instRead: ir.value });
@@ -1181,7 +1181,7 @@
refocus(fresh, key);
});
else if (drawerInst === ir) drawerInst = null;
- return drawerHtml(pick, { body, business, config, pending, instRead: pending ? null : ir.value });
+ return drawerHtml(pick, { body, business, config, pending: pending && ir.n, instRead: pending ? null : ir.value });
}
function drawerHtml(pick, { body, business, config, pending, instRead }) {
const { org, key, id } = pick;
@@ -1253,7 +1253,7 @@
return shell(a.name, `
${fields}
Open tasks
${tasksBody}${tasksAll}
Decisions waiting on you
${decBody}
- ${actions}`, ` data-drawer-org-id="${esc(org)}" data-drawer-name="${esc(a.name)}" data-drawer-org-name="${esc(orgName)}"${pending ? ' data-drawer-pending' : ''}`);
+ ${actions}`, ` data-drawer-org-id="${esc(org)}" data-drawer-name="${esc(a.name)}" data-drawer-org-name="${esc(orgName)}"${pending ? ` data-drawer-pending="${pending}"` : ''}`);
}
// Closing goes Back when a row link on this page pushed the current entry; otherwise it drops
// `agent` from the address in place. Either way the page repaints without it and focus returns to the row.
diff --git a/packages/webui/tests/org-page-fit-browser.test.mjs b/packages/webui/tests/org-page-fit-browser.test.mjs
index 712bfb651..c8c9fdb7b 100644
--- a/packages/webui/tests/org-page-fit-browser.test.mjs
+++ b/packages/webui/tests/org-page-fit-browser.test.mjs
@@ -218,6 +218,22 @@ test('while the drawer\'s instances read is open it says the state is being read
assert.equal(await b.evaluate('document.activeElement?.dataset?.focusKey'), 'drawer-close');
});
+test('SP14 review: another agent opened while the drawer\'s instances read is open repaints when that read answers', async t => {
+ const { b, wait, text, has } = await setup(t, { hash: '#/orgs/acme/organization?agent=acme%3Acoder', inst: 'hold' });
+ await wait(`${settled} && document.querySelector('[data-ws-drawer="acme:coder"][data-drawer-pending]')`);
+ // Chief opens before the held read answers; its drawer waits on the same read.
+ await b.evaluate('location.hash = "#/orgs/acme/organization?agent=acme%3Achief"');
+ const d = '[data-ws-drawer="acme:chief"]';
+ await wait(`document.querySelector('${d}[data-drawer-pending]')`);
+ assert.equal(await text(`${d} [data-drawer-state]`), 'Reading state…');
+ // The read answers. Chief's drawer repaints from it at once, not at the next poll.
+ await b.evaluate('window.__release()');
+ await b.evaluate('new Promise(r => setTimeout(r, 1000))');
+ assert.equal(await has(`${d}[data-drawer-pending]`), false, 'chief\'s drawer still says Reading state… after its read answered');
+ assert.match(await text(`${d} [data-drawer-state]`), /not running/);
+ assert.equal(await has('[data-ws-drawer="acme:coder"]'), false);
+});
+
test('a drawer closed before its instances read answers keeps no answer for the next open', async t => {
const idle = 'window.__heldIdle = true;';
const { b, wait, text } = await setup(t, { hash: '#/orgs/acme/organization?agent=acme%3Acoder', inst: 'hold', scripts: [idle] });