Fix horizontal grid jumping caused by scrollIntoView bubbling

scrollIntoView() in AgentPane was scrolling all ancestor containers
including ProjectGrid (overflow-x: auto), causing the entire project
grid to jump horizontally every time any agent produced output.

Replaced with direct scrollTop/scrollTo manipulation that only affects
the intended scroll container. Also removed scroll-snap-type which
caused additional snap recalculation on layout changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DexterFromLab 2026-03-12 14:32:14 +01:00
parent bb09b3c0ff
commit e8555625ff
3 changed files with 19 additions and 9 deletions

View file

@ -286,7 +286,10 @@
);
if (!msg) return;
autoScroll = false;
scrollContainer.querySelector('#msg-' + CSS.escape(msg.id))?.scrollIntoView({ behavior: 'smooth', block: 'center' });
const el = scrollContainer.querySelector('#msg-' + CSS.escape(msg.id));
if (el instanceof HTMLElement) {
scrollContainer.scrollTop = el.offsetTop - scrollContainer.clientHeight / 2 + el.offsetHeight / 2;
}
}
// Scroll anchoring: two-phase pattern
@ -300,7 +303,7 @@
});
$effect(() => {
if (session?.messages.length !== undefined && wasNearBottom && autoScroll) {
scrollContainer?.querySelector('#message-end')?.scrollIntoView({ behavior: 'instant' as ScrollBehavior });
if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
});
@ -585,7 +588,7 @@
<span class="burn-rate" title="Current session burn rate">${burnRatePerHr.toFixed(2)}/hr</span>
{/if}
{#if !autoScroll}
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollContainer?.querySelector('#message-end')?.scrollIntoView({ behavior: 'instant' as ScrollBehavior }); }}> Bottom</button>
<button class="scroll-btn" onclick={() => { autoScroll = true; if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight; }}> Bottom</button>
{/if}
<button class="stop-btn" data-testid="agent-stop" onclick={handleStop}>Stop</button>
</div>
@ -607,7 +610,7 @@
{/if}
<UsageMeter inputTokens={session.inputTokens} outputTokens={session.outputTokens} contextLimit={DEFAULT_CONTEXT_LIMIT} />
{#if !autoScroll}
<button class="scroll-btn" onclick={() => { autoScroll = true; scrollContainer?.querySelector('#message-end')?.scrollIntoView({ behavior: 'instant' as ScrollBehavior }); }}> Bottom</button>
<button class="scroll-btn" onclick={() => { autoScroll = true; if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight; }}> Bottom</button>
{/if}
</div>
{:else if session.status === 'error'}

View file

@ -390,7 +390,7 @@
display: grid;
grid-template-rows: auto auto 1fr auto;
min-width: 30rem;
scroll-snap-align: start;
/* scroll-snap-align removed: see ProjectGrid */
background: var(--ctp-base);
border: 1px solid var(--ctp-surface0);
border-radius: 0.375rem;

View file

@ -16,14 +16,21 @@
let slotEls = $state<Record<string, HTMLElement>>({});
// Auto-scroll to active project only when activeProjectId changes
// (untrack slotEls so agent re-renders don't trigger unwanted scrolls)
// Uses direct scrollLeft instead of scrollIntoView to avoid bubbling to parent containers
$effect(() => {
const id = activeProjectId;
if (!id) return;
if (!id || !containerEl) return;
untrack(() => {
const el = slotEls[id];
if (!el) return;
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
// Only scroll if the slot is not already visible
const cRect = containerEl!.getBoundingClientRect();
const eRect = el.getBoundingClientRect();
if (eRect.left >= cRect.left && eRect.right <= cRect.right) return;
containerEl!.scrollTo({
left: el.offsetLeft - containerEl!.offsetLeft,
behavior: 'smooth',
});
});
});
@ -75,7 +82,7 @@
gap: 0.25rem;
height: 100%;
overflow-x: auto;
scroll-snap-type: x mandatory;
/* scroll-snap disabled: was causing horizontal jumps when agents auto-scroll */
padding: 0.25rem;
}