diff --git a/.changes/planning/fix-surface-journey-radio-focus-7d2e4b91.md b/.changes/planning/fix-surface-journey-radio-focus-7d2e4b91.md
new file mode 100644
index 00000000000..cf891cc5c2d
--- /dev/null
+++ b/.changes/planning/fix-surface-journey-radio-focus-7d2e4b91.md
@@ -0,0 +1,7 @@
+---
+bump: patch
+---
+
+### Fixed
+
+- The interview page keeps keyboard focus on a choice when a state push redraws the choices. Before, focus fell back to the page whenever Claude's next update arrived while a choice had focus, for example right after Answer again, so the arrow keys stopped moving between the choices.
diff --git a/plugins/planning/surface/index.html b/plugins/planning/surface/index.html
index c893a20eb0f..be324f94881 100644
--- a/plugins/planning/surface/index.html
+++ b/plugins/planning/surface/index.html
@@ -1700,10 +1700,10 @@
Interview
setHtml($("sumRest"), h);
restoreFocus(f);
}
- function focusKey(){ // the focused control, by id or by its data attributes within the nearest region with an id
+ function focusKey(){ // the focused control, by id or by its data attributes (a radio by name and value) within the nearest region with an id
const a = document.activeElement; if (!a || a === document.body || !a.closest) return null;
if (a.id) return {s: "#" + CSS.escape(a.id), start: a.selectionStart, end: a.selectionEnd};
- const attrs = [...a.attributes].filter(x => x.name.startsWith("data-")).map(x => "[" + x.name + '="' + CSS.escape(x.value) + '"]').join(""), box = a.closest("[id]");
+ const attrs = [...a.attributes].filter(x => x.name.startsWith("data-") || (a.type === "radio" && (x.name === "name" || x.name === "value"))).map(x => "[" + x.name + '="' + CSS.escape(x.value) + '"]').join(""), box = a.closest("[id]");
return attrs && box ? {s: "#" + CSS.escape(box.id) + " " + a.tagName.toLowerCase() + attrs, pin: !!a.closest(".sec.pin")} : null;
}
function restoreFocus(f){
diff --git a/plugins/planning/surface/tests/ui_journey.js b/plugins/planning/surface/tests/ui_journey.js
index e9b347f54fb..568d209813f 100644
--- a/plugins/planning/surface/tests/ui_journey.js
+++ b/plugins/planning/surface/tests/ui_journey.js
@@ -240,7 +240,9 @@ async page => { // the user journey in order on one page, no reload after phase
ok("Answer again moves focus to the first choice", await page.evaluate(() => document.activeElement.matches('#choices input[type="radio"]')));
// set up the notice checks: a note for Claude to answer, then the summary with a hiding filter and a hidden rail
- await post({kind: "note", text: "Is the plan on track?"});
+ const r0 = await page.evaluate(() => document.activeElement.value);
+ await post({kind: "note", text: "Is the plan on track?"}); await settle(2000);
+ ok("a state push that rebuilds the choices keeps focus on the same radio", await page.evaluate(v => document.activeElement.matches('#choices input[type="radio"]') && document.activeElement.value === v, r0), await focused());
await page.selectOption("#filter", "answered"); await page.click("#title"); await page.keyboard.press("w"); await page.keyboard.press("["); await settle(300);
ok("setup: the summary shows with the Answered filter and the rail hidden", await sel() === "summary" && await page.$eval("#layout", el => el.classList.contains("norail")), await sel());
}