diff --git a/web/src/structured.ts b/web/src/structured.ts index b8985f1..90b1ba3 100644 --- a/web/src/structured.ts +++ b/web/src/structured.ts @@ -486,6 +486,7 @@ export function createStructuredConnection( const liveGroupIds = new Set([...groups.keys()]); for (const id of expandedGroupIds) if (!liveGroupIds.has(id)) expandedGroupIds.delete(id); + const prevGroupEls = new Map(groupEls); rowEls.clear(); groupEls.clear(); const children: HTMLElement[] = []; @@ -504,23 +505,37 @@ export function createStructuredConnection( rowEls.set(memberRow.id, memberEl); return [memberEl]; }); - const groupEl = buildToolGroup( - groupId, - group.rows, - memberEls, - expandedGroupIds.has(groupId), - (expanded) => { - if (expanded) expandedGroupIds.add(groupId); - else expandedGroupIds.delete(groupId); - settleDisclosureLayout(); - }, - group.toolCount, - ); - if (group.rows.some((r) => enteringRowIds.has(r.id))) { - groupEl.classList.add("st-row-enter"); + + // Reuse existing group element when possible — avoids tearing down + // and recreating the wrapper, which restarts CSS transitions (visible + // flash) and destroys keyboard/focus state. + const existing = prevGroupEls.get(groupId); + if (existing) { + const titleEl = existing.querySelector(".st-tool-group-title"); + if (titleEl) titleEl.textContent = `${group.toolCount} tool calls`; + const inner = existing.querySelector(".st-disclosure-inner"); + if (inner) inner.replaceChildren(...memberEls); + groupEls.set(groupId, existing); + children.push(existing); + } else { + const groupEl = buildToolGroup( + groupId, + group.rows, + memberEls, + expandedGroupIds.has(groupId), + (expanded) => { + if (expanded) expandedGroupIds.add(groupId); + else expandedGroupIds.delete(groupId); + settleDisclosureLayout(); + }, + group.toolCount, + ); + if (group.rows.some((r) => enteringRowIds.has(r.id))) { + groupEl.classList.add("st-row-enter"); + } + groupEls.set(groupId, groupEl); + children.push(groupEl); } - groupEls.set(groupId, groupEl); - children.push(groupEl); continue; } // Resolved approvals not absorbed into a tool group are hidden — the @@ -635,12 +650,14 @@ export function createStructuredConnection( } } + let configDirty = false; function renderConfig(): void { const options = sessionConfigOptions(); configEl.hidden = options.length === 0; if (options.length === 0) { configMenu.hidden = true; configTrigger.setAttribute("aria-expanded", "false"); + configDirty = false; return; } const ordered = [...options].sort((a, b) => { @@ -654,6 +671,15 @@ export function createStructuredConnection( configTrigger.title = summary; configTrigger.setAttribute("aria-label", `Session settings. ${summary.replaceAll("\n", ". ")}`); configTrigger.disabled = configBusy; + + // When the menu is open and the user is interacting (not mid-change), + // skip the DOM rebuild so open