From 94b6e057ed012b91219b6a99c8ad606cc1376045 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:48:04 -0400 Subject: [PATCH 1/3] test(planning): cover the person-held Reconfirm and fix the receipt-line race A Reconfirm of a someone-else-decides deferral with a new name typed over the kept one now has a ui_c check that fails when reconfirmOf drops `who`. The ui_c.6 receipt-line flake: the 2 s tick rendered the header before it asked unheard(), so when the 30 s deaf threshold fell between the two calls the rail chip read "No session is listening" while the receipt line still read "Sent to Claude" until the next tick. The tick now renders the rail first, and the check waits on the receipt line instead of reading it once. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C8UZGiNHApTFuNQ9zKFKk3 --- .../fix-interview-web-view-followups-3c7d91a2.md | 7 +++++++ plugins/planning/surface/index.html | 2 +- plugins/planning/surface/surface.test.sh | 2 +- plugins/planning/surface/tests/ui_c.js | 10 +++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 .changes/planning/fix-interview-web-view-followups-3c7d91a2.md diff --git a/.changes/planning/fix-interview-web-view-followups-3c7d91a2.md b/.changes/planning/fix-interview-web-view-followups-3c7d91a2.md new file mode 100644 index 0000000000..8280693887 --- /dev/null +++ b/.changes/planning/fix-interview-web-view-followups-3c7d91a2.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- The interview page's receipt line and question chips now switch to "No session is listening" on the same tick; before, the receipt line could lag the chip by up to two seconds. diff --git a/plugins/planning/surface/index.html b/plugins/planning/surface/index.html index 74933e7f72..23ec08c0cf 100644 --- a/plugins/planning/surface/index.html +++ b/plugins/planning/surface/index.html @@ -2113,7 +2113,7 @@

Interview

}); window.addEventListener("resize", () => grow($("note"))); let wasUnheard = false; - setInterval(() => { if (!S.loaded) return; renderHeader(); if (unheard() !== wasUnheard) { wasUnheard = unheard(); renderRail(); } }, 2000); + setInterval(() => { if (!S.loaded) return; if (unheard() !== wasUnheard) { wasUnheard = unheard(); renderRail(); } renderHeader(); }, 2000); // header last: unheard() only turns true as time passes, so a header read after the rail never lags it applySettings(); refresh().then(() => setOnline(true)).catch(() => { setOnline(false); restoreFinish(); }).finally(connect); diff --git a/plugins/planning/surface/surface.test.sh b/plugins/planning/surface/surface.test.sh index 1df7ff6f12..1f2c7c63c3 100755 --- a/plugins/planning/surface/surface.test.sh +++ b/plugins/planning/surface/surface.test.sh @@ -374,7 +374,7 @@ if command -v playwright-cli >/dev/null 2>&1; then for n in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do grade "ui_journey.$n" "$tmp/uj$n.out"; done for n in 1 2 3 4; do grade "ui_live.$n" "$tmp/ul$n.out"; done else - browser=472 journey=222 + browser=473 journey=222 echo "SKIP: $browser browser checks not run, $journey of them the journey (playwright-cli not found)" # silent-skip-ok: browser checks need a local playwright-cli # discriminating-skip-ok: the API, watcher and hygiene checks above still grade this suite skip=$((skip + browser)) fi diff --git a/plugins/planning/surface/tests/ui_c.js b/plugins/planning/surface/tests/ui_c.js index c6537b20b6..be0a366ba7 100644 --- a/plugins/planning/surface/tests/ui_c.js +++ b/plugins/planning/surface/tests/ui_c.js @@ -236,6 +236,13 @@ async page => { await page.click("main.detail h3"); await page.keyboard.press("Control+Enter"); await page.waitForTimeout(800); const ev4 = await last(); ok("an armed alternative survives a Reconfirm row shifting the key numbers: (b) stays armed once stale and saves alt b", /^\(b\)/.test(a0) && wasStale && /^\(b\)/.test(a1) && ev4.id === "P2" && ev4.kind === "alt" && ev4.alt === "b", a0 + " / " + a1 + " " + JSON.stringify(ev4)); + // Reconfirm of a person-held deferral with a new name typed over the kept one stays person-held + await restale({kind: "defer", text: "someone else decides: Dana"}, {kind: "alt", alt: "b"}); + await page.fill("#note", "Lee, the architect"); + await page.click("main.detail h3"); await page.keyboard.press("a"); + const r7 = (await armed()).trim(); await page.keyboard.press("Control+Enter"); await page.waitForTimeout(800); + const ev7 = await last(); + ok("Reconfirm of a person-held deferral with a new name keeps the someone else decides prefix", /^Reconfirm/.test(r7) && ev7.id === "P2" && ev7.kind === "defer" && ev7.text === "someone else decides: Lee, the architect", r7 + " " + JSON.stringify(ev7)); // Reconfirm with a note typed over the kept one records the typed note await restale({kind: "alt", alt: "b", text: "Only on weekends"}, {kind: "accept"}); await page.fill("#note", "Weekdays too"); @@ -500,7 +507,8 @@ async page => { ok("a fresh answer, with no watcher silent for 30 s yet, reads Sent to Claude", /Sent to Claude/.test(await chip()), await chip()); const quiet = await page.waitForFunction(() => /No session is listening; type next in the terminal/.test(document.querySelector('.qbtn[data-q="D1"]').textContent), null, {timeout: 45000}).then(() => true).catch(() => false); ok("with no watcher holding the lease, an unhandled answer reads as not delivered", quiet && !/Sent to Claude/.test(await chip()), await chip()); - ok("the receipt line says the same", /No session is listening/.test(await page.textContent("#claudeLine")), await page.textContent("#claudeLine")); + const line = await page.waitForFunction(() => /No session is listening/.test(document.getElementById("claudeLine").textContent), null, {timeout: 5000}).then(() => true).catch(() => false); + ok("the receipt line says the same", line, await page.textContent("#claudeLine")); // The watcher claims the lease; the shell handles the event a few seconds later. const r = await page.request.get(base + "api/wait?after=handled&timeout=1&watcher=w1", {headers: {"X-Interview-Token": await token()}}); const back = await page.waitForFunction(() => /Sent to Claude/.test(document.querySelector('.qbtn[data-q="D1"]').textContent), null, {timeout: 12000}).then(() => true).catch(() => false); From 2a0c89d01618e54005c843c95f465848e8636c84 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:59:09 -0400 Subject: [PATCH 2/3] docs(planning): scope the receipt-line changelog line to the timer tick Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C8UZGiNHApTFuNQ9zKFKk3 --- .changes/planning/fix-interview-web-view-followups-3c7d91a2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/planning/fix-interview-web-view-followups-3c7d91a2.md b/.changes/planning/fix-interview-web-view-followups-3c7d91a2.md index 8280693887..177dbf8045 100644 --- a/.changes/planning/fix-interview-web-view-followups-3c7d91a2.md +++ b/.changes/planning/fix-interview-web-view-followups-3c7d91a2.md @@ -4,4 +4,4 @@ bump: patch ### Fixed -- The interview page's receipt line and question chips now switch to "No session is listening" on the same tick; before, the receipt line could lag the chip by up to two seconds. +- When no session has listened for 30 seconds, the interview page's timer now updates the question chips and the receipt line together; before, the receipt line could keep "Sent to Claude" for up to two seconds after a chip read "No session is listening". From b7f10b602cfdeadef995657236da0b774c7d1151 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:18:16 -0400 Subject: [PATCH 3/3] fix(planning): keep the interview header and rail in step on the flip tick Rendering the rail before the header still let the header lead the rail when unheard() was false at the check and turned true inside renderHeader(). The tick now renders the header, reads unheard() once, and on a flip redraws the rail and then the header. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C8UZGiNHApTFuNQ9zKFKk3 --- plugins/planning/surface/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/planning/surface/index.html b/plugins/planning/surface/index.html index 23ec08c0cf..85c28b72f8 100644 --- a/plugins/planning/surface/index.html +++ b/plugins/planning/surface/index.html @@ -2113,7 +2113,7 @@

Interview

}); window.addEventListener("resize", () => grow($("note"))); let wasUnheard = false; - setInterval(() => { if (!S.loaded) return; if (unheard() !== wasUnheard) { wasUnheard = unheard(); renderRail(); } renderHeader(); }, 2000); // header last: unheard() only turns true as time passes, so a header read after the rail never lags it + setInterval(() => { if (!S.loaded) return; renderHeader(); const u = unheard(); if (u !== wasUnheard) { wasUnheard = u; renderRail(); renderHeader(); } }, 2000); // unheard() turns true as time passes, so the header is redrawn after the rail on the tick it flips applySettings(); refresh().then(() => setOnline(true)).catch(() => { setOnline(false); restoreFinish(); }).finally(connect);