From b7d85c70bb6ffbbeae30fac35171a30a02e40a3d Mon Sep 17 00:00:00 2001 From: "melodic-automation-lanes[bot]" <337595936+melodic-automation-lanes[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 05:57:39 +0000 Subject: [PATCH 1/4] Resolve issue #11843 Lane-Activity: work-resolve-issue/resolve-issue --- .../work-resolve-issue-issue-11843-5b1e9c3d.md | 7 +++++++ plugins/planning/surface/index.html | 13 ++++++++----- plugins/planning/surface/surface-checks.sh | 2 +- plugins/planning/surface/tests/ui_journey.js | 4 ++++ 4 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 .changes/planning/work-resolve-issue-issue-11843-5b1e9c3d.md diff --git a/.changes/planning/work-resolve-issue-issue-11843-5b1e9c3d.md b/.changes/planning/work-resolve-issue-issue-11843-5b1e9c3d.md new file mode 100644 index 00000000000..70e572377a7 --- /dev/null +++ b/.changes/planning/work-resolve-issue-issue-11843-5b1e9c3d.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- The interview page's Notes panel keeps its send message (for example "Queued, Claude is busy") when a state push redraws the panel. Before, the message went blank as soon as the next push arrived, often before it could be read. "Queued, Claude is busy" drops off once Claude is no longer busy with the earlier wake. A failed send clears the message, and so does leaving the Notes panel. diff --git a/plugins/planning/surface/index.html b/plugins/planning/surface/index.html index 60b49e0b48f..28ac3b02fbf 100644 --- a/plugins/planning/surface/index.html +++ b/plugins/planning/surface/index.html @@ -621,7 +621,7 @@

Interview

listener: {state: "idle"}, online: false, session: "", sel: null, pick: new Set(), busy: false, toast: "", toastBad: false, toastUndo: false, view: "groups", filter: "all", search: "", fly: null, pinned: null, railHidden: false, vtab: null, api: 1, settings: {}, watchSeq: 0, stateAt: 0, instance: "", restarted: false, finished: null, need: null, actKeys: null, - armed: null, snap: {}, conflict: null, undo: null, propEdit: null, propDraft: null, propMsg: null, revs: null, themeSig: "", loaded: false, replies: {}, noteSeen: 0, noteOpen: new Set(), moreShut: new Set(), hiddenBase: null, vfiles: {}, gal: {a: null, b: null, on: false}}; + armed: null, snap: {}, conflict: null, undo: null, propEdit: null, propDraft: null, propMsg: null, revs: null, themeSig: "", loaded: false, replies: {}, noteSeen: 0, noteToast: "", noteQueued: false, noteOpen: new Set(), moreShut: new Set(), hiddenBase: null, vfiles: {}, gal: {a: null, b: null, on: false}}; let Q = {}; // ---------- storage (every access guarded) ---------- @@ -1823,6 +1823,7 @@

Interview

(log.length ? '
Log (' + log.length + ")" + rows(log) + "
" : ""); if (un.size) { markSeen(); renderHeader(); renderRail(); } } + const noteToastText = () => S.noteToast + (S.noteQueued ? queued() : ""); // the busy note lasts only while the wake it reported is in flight function renderNotes(body){ const nt = noteThread(), draft = lsGet("noteDraft", ""); S.noteSeen = Date.now(); lsSet("noteSeen", S.noteSeen); @@ -1841,7 +1842,7 @@

Interview

(fold ? '
' : "") + '
' + (nt.length ? nt.map(noteHtml).join("") : '

No notes yet.

') + "
" + '" + - '
' + (S.api < 2 ? RESTART : closed() ? closedWhy() : QuestionShape.keyHint("send")) + '
'; + '
' + (S.api < 2 ? RESTART : closed() ? closedWhy() : QuestionShape.keyHint("send")) + '
' + esc(noteToastText()) + "
"; // held in state, so a re-render on a state push keeps it const ta = $("noteText"), nk = noteBox(ta, "send"); ta.nextElementSibling.append(nk.count); Kit.grow(ta); const th = body.querySelector(".nthread"); if (th.lastElementChild) th.lastElementChild.scrollIntoView({block: "nearest"}); } @@ -1880,7 +1881,7 @@

Interview

$("keysBody").innerHTML = KEYS.map(([k, t]) => "" + esc(k) + "" + esc(t) + "").join(""); $("keysDlg").showModal(); } - function showFly(p){ S.fly = p; lsSet("fly", p); renderFly(); renderHeader(); } + function showFly(p){ if (p !== "notes") S.noteToast = ""; S.fly = p; lsSet("fly", p); renderFly(); renderHeader(); } const openPanel = p => showFly(S.fly === p ? null : p); function focusHead(){ const h = $("qhead"); if (h) { h.tabIndex = -1; h.focus(); } } @@ -2298,8 +2299,10 @@

Interview

async function sendNote(){ const ta = $("noteText"); if (!ta) return; const text = ta.value.trim(); - if (!text) { $("noteToast").textContent = "Write a note first."; return; } - if (await sendFree("note", text)) { lsSet("noteDraft", null); if (S.fly === "notes") renderFly(); const t = $("noteToast"); if (t) t.textContent = "Sent. Claude's reply lands here." + queued(); } + const say = (msg, q) => { if (S.fly !== "notes") msg = ""; S.noteToast = msg; S.noteQueued = !!q; const t = $("noteToast"); if (t) t.textContent = noteToastText(); }; // a send that finishes after the panel closed holds nothing + if (!text) { say("Write a note first."); return; } + if (await sendFree("note", text)) { lsSet("noteDraft", null); if (S.fly === "notes") renderFly(); say("Sent. Claude's reply lands here.", queued()); } + else say(""); // a failed send must not leave the last send's toast standing } const carried = q => String(lsGet("draft:" + q.id, null) || "").trim(); // what a single Accept would send: the note typed on it function openedRev(q){ // the revision the user last opened q at, with the decisions made since diff --git a/plugins/planning/surface/surface-checks.sh b/plugins/planning/surface/surface-checks.sh index 9c1b23f13c8..1c0ee505eab 100755 --- a/plugins/planning/surface/surface-checks.sh +++ b/plugins/planning/surface/surface-checks.sh @@ -25,7 +25,7 @@ bad() { echo "FAIL: $1"; fail=$((fail + 1)); } # The browser groups of each part, and each group's check count for the SKIP line. declare -A PARTS=([main]="ab e live" [page]="page" [ui-c]="c" [ui-c-listen]="c_listen" [ui-c-ledger]="c_ledger" [journey]="journey") -declare -A CHECKS=([ab]=97 [c]=176 [c_listen]=21 [c_ledger]=10 [e]=22 [journey]=235 [live]=18 [page]=122) +declare -A CHECKS=([ab]=97 [c]=176 [c_listen]=21 [c_ledger]=10 [e]=22 [journey]=236 [live]=18 [page]=122) part=main case "$#:${1:-}" in 0:) ;; diff --git a/plugins/planning/surface/tests/ui_journey.js b/plugins/planning/surface/tests/ui_journey.js index e616592f8be..08226e2341e 100644 --- a/plugins/planning/surface/tests/ui_journey.js +++ b/plugins/planning/surface/tests/ui_journey.js @@ -717,6 +717,10 @@ async page => { // the user journey in order on one page, no reload after phase await page.evaluate(() => document.querySelector('[data-panel="notes"]').click()); await settle(300); await page.fill("#noteText", "second note"); await page.click("[data-sendnote]"); ok("a save while a wake is in flight says queued, Claude is busy", await until(() => /Queued, Claude is busy/.test((document.getElementById("noteToast") || {}).textContent || ""), 4000), await text("#noteToast")); + const frame = await page.evaluate(() => window.__io.applied); + await page.request.get((await page.evaluate(() => location.origin)) + "/api/wait?after=0&timeout=1", {headers: {"X-Interview-Token": await token()}}); // delivers the second note, so the server pushes a state frame + const pushed = await page.waitForFunction(f => window.__io.applied !== f, frame, {timeout: 5000, polling: 50}).then(() => true).catch(() => false); + ok("the Notes send toast outlasts the next state push", pushed && /Queued, Claude is busy/.test(await text("#noteToast")), (pushed ? "" : "no frame; ") + await text("#noteToast")); await page.click("#flyClose", {timeout: 2000}).catch(() => {}); } if (PHASE === 17) { // after the wrap-up the user confirms the third restatement; the shell has written nothing since phase 16 From 4283c441460557cbd48806d7b4efd2bed69f9180 Mon Sep 17 00:00:00 2001 From: "melodic-automation-lanes[bot]" <337595936+melodic-automation-lanes[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:37:00 +0000 Subject: [PATCH 2/4] chore: start the lane checks again on main's workflow files The token broker refused a lane run on the previous head as workflow-modified (.github/workflows/pr-run-activity-write.yml). This commit changes no file. From 4071cfa4477a952387418b8c24e5992d0a3ed95c Mon Sep 17 00:00:00 2001 From: "melodic-automation-lanes[bot]" <337595936+melodic-automation-lanes[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:02:36 +0000 Subject: [PATCH 3/4] Address review feedback on work-resolve-issue/issue-11843-41275815373-1 Lane-Activity: pr-unblock/address-feedback Lane-Thread: PRRT_kwDOTCGFQM6yYBsx --- plugins/planning/surface/index.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/planning/surface/index.html b/plugins/planning/surface/index.html index 28ac3b02fbf..b20140cb2ca 100644 --- a/plugins/planning/surface/index.html +++ b/plugins/planning/surface/index.html @@ -621,7 +621,7 @@

Interview

listener: {state: "idle"}, online: false, session: "", sel: null, pick: new Set(), busy: false, toast: "", toastBad: false, toastUndo: false, view: "groups", filter: "all", search: "", fly: null, pinned: null, railHidden: false, vtab: null, api: 1, settings: {}, watchSeq: 0, stateAt: 0, instance: "", restarted: false, finished: null, need: null, actKeys: null, - armed: null, snap: {}, conflict: null, undo: null, propEdit: null, propDraft: null, propMsg: null, revs: null, themeSig: "", loaded: false, replies: {}, noteSeen: 0, noteToast: "", noteQueued: false, noteOpen: new Set(), moreShut: new Set(), hiddenBase: null, vfiles: {}, gal: {a: null, b: null, on: false}}; + armed: null, snap: {}, conflict: null, undo: null, propEdit: null, propDraft: null, propMsg: null, revs: null, themeSig: "", loaded: false, replies: {}, noteSeen: 0, noteToast: "", noteQueued: [], noteOpen: new Set(), moreShut: new Set(), hiddenBase: null, vfiles: {}, gal: {a: null, b: null, on: false}}; let Q = {}; // ---------- storage (every access guarded) ---------- @@ -1823,7 +1823,7 @@

Interview

(log.length ? '
Log (' + log.length + ")" + rows(log) + "
" : ""); if (un.size) { markSeen(); renderHeader(); renderRail(); } } - const noteToastText = () => S.noteToast + (S.noteQueued ? queued() : ""); // the busy note lasts only while the wake it reported is in flight + const noteToastText = () => S.noteToast + (S.noteQueued.some(s => !isHandled(s)) ? queued() : ""); // the busy note lasts only while the wake it reported is in flight: S.noteQueued holds the seqs that wake was handling when the note was sent function renderNotes(body){ const nt = noteThread(), draft = lsGet("noteDraft", ""); S.noteSeen = Date.now(); lsSet("noteSeen", S.noteSeen); @@ -2299,9 +2299,10 @@

Interview

async function sendNote(){ const ta = $("noteText"); if (!ta) return; const text = ta.value.trim(); - const say = (msg, q) => { if (S.fly !== "notes") msg = ""; S.noteToast = msg; S.noteQueued = !!q; const t = $("noteToast"); if (t) t.textContent = noteToastText(); }; // a send that finishes after the panel closed holds nothing + const say = (msg, q) => { if (S.fly !== "notes") msg = ""; S.noteToast = msg; S.noteQueued = q || []; const t = $("noteToast"); if (t) t.textContent = noteToastText(); }; // a send that finishes after the panel closed holds nothing if (!text) { say("Write a note first."); return; } - if (await sendFree("note", text)) { lsSet("noteDraft", null); if (S.fly === "notes") renderFly(); say("Sent. Claude's reply lands here.", queued()); } + const inFlight = pending().filter(delivered).map(e => e.seq); // the wake in flight before this note, never the note itself + if (await sendFree("note", text)) { lsSet("noteDraft", null); if (S.fly === "notes") renderFly(); say("Sent. Claude's reply lands here.", queued() ? inFlight : []); } else say(""); // a failed send must not leave the last send's toast standing } const carried = q => String(lsGet("draft:" + q.id, null) || "").trim(); // what a single Accept would send: the note typed on it From 56c5a3f16990bf85ab9878e34678238e36f8dd18 Mon Sep 17 00:00:00 2001 From: "melodic-automation-lanes[bot]" <337595936+melodic-automation-lanes[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:59:54 +0000 Subject: [PATCH 4/4] Address review feedback on work-resolve-issue/issue-11843-41275815373-1 Lane-Activity: pr-unblock/address-feedback Lane-Thread: PRRT_kwDOTCGFQM6yXweM --- plugins/planning/surface/index.html | 4 ++-- plugins/planning/surface/tests/ui_journey.js | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/planning/surface/index.html b/plugins/planning/surface/index.html index b20140cb2ca..c893a20eb0f 100644 --- a/plugins/planning/surface/index.html +++ b/plugins/planning/surface/index.html @@ -1881,7 +1881,7 @@

Interview

$("keysBody").innerHTML = KEYS.map(([k, t]) => "" + esc(k) + "" + esc(t) + "").join(""); $("keysDlg").showModal(); } - function showFly(p){ if (p !== "notes") S.noteToast = ""; S.fly = p; lsSet("fly", p); renderFly(); renderHeader(); } + function showFly(p){ if (p !== "notes") { S.noteToast = ""; S.noteQueued = []; } S.fly = p; lsSet("fly", p); renderFly(); renderHeader(); } const openPanel = p => showFly(S.fly === p ? null : p); function focusHead(){ const h = $("qhead"); if (h) { h.tabIndex = -1; h.focus(); } } @@ -2299,7 +2299,7 @@

Interview

async function sendNote(){ const ta = $("noteText"); if (!ta) return; const text = ta.value.trim(); - const say = (msg, q) => { if (S.fly !== "notes") msg = ""; S.noteToast = msg; S.noteQueued = q || []; const t = $("noteToast"); if (t) t.textContent = noteToastText(); }; // a send that finishes after the panel closed holds nothing + const say = (msg, q) => { if (S.fly !== "notes") { msg = ""; q = []; } S.noteToast = msg; S.noteQueued = q || []; const t = $("noteToast"); if (t) t.textContent = noteToastText(); }; // a send that finishes after the panel closed holds nothing if (!text) { say("Write a note first."); return; } const inFlight = pending().filter(delivered).map(e => e.seq); // the wake in flight before this note, never the note itself if (await sendFree("note", text)) { lsSet("noteDraft", null); if (S.fly === "notes") renderFly(); say("Sent. Claude's reply lands here.", queued() ? inFlight : []); } diff --git a/plugins/planning/surface/tests/ui_journey.js b/plugins/planning/surface/tests/ui_journey.js index a1ec9dc6f72..e9b347f54fb 100644 --- a/plugins/planning/surface/tests/ui_journey.js +++ b/plugins/planning/surface/tests/ui_journey.js @@ -728,6 +728,11 @@ async page => { // the user journey in order on one page, no reload after phase const pushed = await page.waitForFunction(f => window.__io.applied !== f, frame, {timeout: 5000, polling: 50}).then(() => true).catch(() => false); ok("the Notes send toast outlasts the next state push", pushed && /Queued, Claude is busy/.test(await text("#noteToast")), (pushed ? "" : "no frame; ") + await text("#noteToast")); await page.click("#flyClose", {timeout: 2000}).catch(() => {}); + // Leaving Notes clears the send toast and its busy note together, so coming back while that wake is still in flight shows neither. + const closed = !(await page.$eval("#fly", el => el.classList.contains("open"))), busy = /^Claude is working on /.test(await text("#workLine")); + await page.evaluate(() => document.querySelector('[data-panel="notes"]').click()); await settle(300); + ok("reopening Notes while a wake is in flight shows no busy note on its own", closed && busy && await text("#noteToast") === "", (closed ? "" : "Notes did not close; ") + (busy ? "" : "no wake in flight; ") + JSON.stringify(await text("#noteToast"))); + await page.click("#flyClose", {timeout: 2000}).catch(() => {}); } if (PHASE === 17) { // after the wrap-up the user confirms the third restatement; the shell has written nothing since phase 16 await page.setViewportSize({width: 1400, height: 860}); await tap("#sumBtn", 300);