▾ ' + esc(s.title) + ' ' + s.open + " open / " + s.items.length + " " +
+ '
▾ ' + esc(s.title) + ' ' + s.open + " open / " + s.items.length + (s.review ? ", " + s.review + " to review" : "") + " " +
(elig.length && aa ? 'Accept all (' + elig.length + ") " : "") +
(elig.length && s.round && !s.gid ? 'Accept all and have agents check them ' : "") + "
" +
(s.lock.length ? '
Locked: opens after ' + s.lock.map(g => esc(groupTitle(g))).join(", ") + "
" : "") +
@@ -793,9 +794,10 @@
Interview
list.scrollTop = top;
(flashIds || []).forEach(id => list.querySelectorAll('.qbtn[data-q="' + CSS.escape(id) + '"]').forEach(el => el.classList.add("flash")));
}
- function railOrder(){
- if (S.view === "tree") { const t = treeData(), out = [], walk = id => { if (out.includes(id)) return; out.push(id); t.kids[id].forEach(walk); }; t.roots.forEach(walk); return out.filter(id => passes(Q[id])); }
- return sections().secs.flatMap(s => s.items.filter(passes).map(q => q.id));
+ function railOrder(all){ // all: every question in rail order, so a just-answered question still marks the position
+ const keep = id => all || passes(Q[id]);
+ if (S.view === "tree") { const t = treeData(), out = [], walk = id => { if (out.includes(id)) return; out.push(id); t.kids[id].forEach(walk); }; t.roots.forEach(walk); return out.filter(keep); }
+ return sections().secs.flatMap(s => s.items.map(q => q.id)).filter(keep);
}
function revealInRail(id){
const list = $("railList"), el = list.querySelector(id === SUMMARY ? ".sumbtn" : '.qbtn[data-q="' + CSS.escape(id) + '"]');
@@ -1551,8 +1553,8 @@
Interview
return need.find(id => !blocked(Q[id])) || need[0] || order[0] || null;
}
function nextNeeding(dir){ // next (1) or previous (-1) item needing you, in rail order
- const order = railOrder(), len = order.length, i = order.indexOf(S.sel);
- for (let k = 1; k <= len; k++) { const id = order[(((i < 0 && dir < 0 ? len : i) + dir * k) % len + len) % len]; if (id !== S.sel && needsYou(Q[id])) return id; }
+ const order = railOrder(true), len = order.length, i = order.indexOf(S.sel);
+ for (let k = 1; k <= len; k++) { const id = order[(((i < 0 && dir < 0 ? len : i) + dir * k) % len + len) % len]; if (id !== S.sel && needsYou(Q[id]) && passes(Q[id])) return id; }
return null;
}
function armKind(kind){ // a arms Reconfirm when there is one, else Accept
@@ -1874,7 +1876,7 @@
Interview
if (first) {
S.loaded = true;
S.view = lsGet("view", "groups"); if (!["groups", "rounds", "tree"].includes(S.view)) S.view = "groups";
- S.filter = lsGet("filter", "all"); if (!["all", "open", "pending", "answered"].includes(S.filter)) S.filter = "all";
+ S.filter = lsGet("filter", "all"); if (!["all", "open", "review", "pending", "answered"].includes(S.filter)) S.filter = "all";
S.fly = lsGet("fly", null); if (!["visuals", "notes", "activity", "settings"].includes(S.fly)) S.fly = null;
S.pinned = lsGet("pinned", false); S.railHidden = lsGet("railHidden", false); S.noteSeen = lsGet("noteSeen", 0);
if (lsGet("actSeen", null) === null) markSeen(); // a new browser starts with everything seen
diff --git a/plugins/planning/surface/tests/ui_c.js b/plugins/planning/surface/tests/ui_c.js
index 866de06037..5893c027a4 100644
--- a/plugins/planning/surface/tests/ui_c.js
+++ b/plugins/planning/surface/tests/ui_c.js
@@ -162,14 +162,17 @@ async page => {
await page.keyboard.press("Shift+N");
ok("AC16: Shift+N goes back (P2)", await sel() === "P2", await sel());
- // R1 holds an accept followed by Claude's reply to an ask: it still needs you, so Show: Open lists it and the group counts it
+ // R1 holds an accept followed by Claude's reply to an ask: it is answered, so Show: Open leaves it out and Show: Review lists it
await page.selectOption("#filter", "open"); await page.waitForTimeout(200);
const openIds = await page.$$eval(".rail-list .qbtn", els => els.map(e => e.dataset.q));
- ok("Show: Open lists an accepted question with an unanswered Claude reply", openIds.includes("R1"), openIds.join(","));
+ ok("Show: Open leaves out an accepted question with an unanswered Claude reply", !openIds.includes("R1"), openIds.join(","));
ok("Show: Open leaves out a settled question", !openIds.includes("P1"), openIds.join(","));
ok("Show: Open leaves out a question held for research, whose newest Claude line is the hold", !openIds.includes("H1"), openIds.join(","));
+ await page.selectOption("#filter", "review"); await page.waitForTimeout(200);
+ const reviewIds = await page.$$eval(".rail-list .qbtn", els => els.map(e => e.dataset.q));
+ ok("Show: Review lists the accepted question with an unanswered Claude reply and no unanswered one", reviewIds.includes("R1") && !reviewIds.includes("P1"), reviewIds.join(","));
ok("no Sent to Claude chip once a reply carries replyTo at or past the last event", !/Sent to Claude/.test(await page.textContent('.qbtn[data-q="R1"]')), await page.textContent('.qbtn[data-q="R1"]'));
- ok("the group counter counts the unanswered reply", /^1 open \//.test(await page.textContent('.sec[data-key="g:talk"] .cnt')), await page.textContent('.sec[data-key="g:talk"] .cnt'));
+ ok("the group counter counts the unanswered reply as to review, not open", /^0 open \/ \d+, 1 to review/.test(await page.textContent('.sec[data-key="g:talk"] .cnt')), await page.textContent('.sec[data-key="g:talk"] .cnt'));
ok("a reply after the accept puts the after-answer chip on the card", /Replied after your answer/.test(await page.textContent('.qbtn[data-q="R1"]')), await page.textContent('.qbtn[data-q="R1"]'));
await pick("R1");
ok("an accepted question has input:checked on the recommended row, labeled Your answer", await page.$eval("#choices .choice.rec", el => el.querySelector("input:checked") !== null && /Your answer/.test(el.textContent)), await page.textContent("#choices"));
diff --git a/plugins/planning/surface/tests/ui_journey.js b/plugins/planning/surface/tests/ui_journey.js
index f2a1fdf970..af58f8c287 100644
--- a/plugins/planning/surface/tests/ui_journey.js
+++ b/plugins/planning/surface/tests/ui_journey.js
@@ -243,9 +243,11 @@ async page => { // the user journey in order on one page, no reload after phase
const f4 = await page.$eval("#filter", el => el.value);
await page.selectOption("#filter", "open"); await page.waitForTimeout(200);
const open4 = (await page.$$eval(".rail-list .qbtn", els => els.map(e => e.dataset.q))).join(",");
+ await page.selectOption("#filter", "review"); await page.waitForTimeout(200);
+ const review4 = (await page.$$eval(".rail-list .qbtn", els => els.map(e => e.dataset.q))).join(",");
await page.selectOption("#filter", f4); await page.waitForTimeout(150);
const cards4 = await text('.qbtn[data-q="Q2"]') + await text('.qbtn[data-q="Q4"]');
- ok("Claude's confirm-commitments line after an answer is not a reply: Q2 and Q4 stay settled, off Show: Open and without the after-answer chip", !/Q[24]/.test(open4) && !/after your answer/.test(cards4) && /Q3/.test(open4) && (await text("#meterText")) === "4 of 7 answered", open4 + " / " + cards4 + " / " + await text("#meterText"));
+ ok("Claude's confirm-commitments line after an answer is not a reply: Q2 and Q4 stay settled, off Show: Open and Show: Review and without the after-answer chip; Q3, answered with a later reply, is under Review", !/Q[24]/.test(open4 + review4) && !/after your answer/.test(cards4) && /Q3/.test(review4) && !/Q3/.test(open4) && (await text("#meterText")) === "4 of 7 answered", open4 + " / " + review4 + " / " + cards4 + " / " + await text("#meterText"));
ok("Wrap up warns while assumptions are open and stays enabled", /^3 assumptions not confirmed yet\.$/.test(await text("#openAssumeWarn")) && !(await page.$eval('[data-wrapup="1"]', el => el.disabled)), await text("#openAssumeWarn"));
const n1 = (await events()).length;
await tap("[data-confirmall]", 1500);