fix: the band's inset survives a resize made in full screen

The panel was remembered as a width, sampled while it stood still. A window
resized during full screen left that width describing a layout that no longer
existed, and the way out pinned the band to it for the length of the move. It
is now remembered as the share of the row the panel takes, which the new row
width turns back into the right inset: leaving full screen at 1200px after
entering it at 1600px, the band starts at 628 rather than at 909.

The pane transition's duration is read from the property the timer is set from,
so the class cannot outlive the movement it describes, and two comments that
described earlier drafts are rewritten: the band's name in full screen comes
from the session's own header once it has mounted, and the tab strip's wrapper
carries a fade, not a measured move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-05 11:47:35 -07:00
co-authored by Claude Opus 5
parent bf80473d35
commit e648b15ebd
2 changed files with 33 additions and 29 deletions
@@ -197,8 +197,7 @@ same weight, size and icon size, so the line reads as one control rather than fo
<!-- bind:clientWidth: the menu hangs from the chevron, so it is shifted back by the width of
the name to line its left edge up with the disc. -->
<div class="flex items-center min-w-0" bind:clientWidth={nameWidth}>
<svelte:element
this={'a'}
<a
href={homeHref}
class={scopeChipClass}
title={showFork ? `${familyName} / ${scopeName}` : familyName}
@@ -231,7 +230,7 @@ same weight, size and icon size, so the line reads as one control rather than fo
{@render envBadgeMark()}
{/if}
{/if}
</svelte:element>
</a>
</div>
<Menubar>
{#snippet children({ createMenu })}
@@ -467,12 +467,7 @@
if (reducedMotion.val) return
panesAnimating = true
clearTimeout(panesAnimationTimer)
panesAnimationTimer = setTimeout(() => {
panesAnimating = false
// The panel's final width was measured while the writes above were suppressed, and a
// settled panel may get no further resize to publish it.
if (!fullscreen) panelWidth = previewWidth
}, PANES_MOVE_MS)
panesAnimationTimer = setTimeout(() => (panesAnimating = false), PANES_MOVE_MS)
}
onDestroy(() => clearTimeout(panesAnimationTimer))
/** The arriving strip fades in over the expansion; the leaving one goes quickly, so the two
@@ -490,12 +485,21 @@
}
// Width of the preview panel, so the band can stop where it starts.
let previewWidth = $state(0)
/** The same width, excluding what the panel measures while it is expanding or contracting for
* a full-screen switch. The band's inset is read from this: fed the live measurement, the bar
* would be sized against a panel that has not arrived yet, and on the way back from full
* screen it starts at 146px — narrow enough that the bar wraps to two rows for a tenth of a
* second before the panel settles. */
let panelWidth = $state(0)
/** Width of the row the panes share, so the panel can be remembered as a share of it. */
let contentWidth = $state(0)
/** What share of that row the panel takes, sampled only while it is standing still. The band's
* inset is read from this for the length of a full-screen switch: fed the live measurement,
* the bar would be sized against a panel that has not arrived yet, and on the way back from
* full screen it starts at 146px — narrow enough that the bar wraps to two rows before the
* panel settles. A share rather than a width, so a window resized while full screen still
* gives the right inset on the way out. */
let panelShare = $state(0)
const bandInset = $derived.by(() => {
if (fullscreen) return 0
if (panesAnimating && panelShare > 0 && contentWidth > 0)
return Math.round(panelShare * contentWidth)
return previewCollapsed || previewWidth === 0 ? undefined : previewWidth
})
// Fullscreen is page state, not per-session, so it outlives a session switch —
// tell the incoming session's model, whose own collapsed flag it overrides, or
// re-opening the item plainly on screen would be judged invisible and not flash.
@@ -955,15 +959,12 @@
<!-- The session showing fills the band's breadcrumb. The band is the layout's, but it stops
where the preview panel starts so that panel can run from the top of the viewport. -->
<!-- Full screen drops the chat column, and with it the SessionWrapper that names the band — so the
page names it instead, and hands the band the preview's own strip and controls as one filling
surface. -->
<!-- In full screen the band carries the session's name and, as one filling surface, the preview's
own strip and controls. The name here is only what shows until the active session's
SessionWrapper has mounted: it registers after this does, and the registry merges later-wins,
so from then on the name comes from there, with its pen and its menu. -->
<PageHeaderContent
barRightInset={fullscreen
? 0
: previewCollapsed || panelWidth === 0
? undefined
: panelWidth}
barRightInset={bandInset}
section={fullscreen ? { label: activeSession?.summary ?? 'Untitled session' } : undefined}
actions={fullscreen ? fullscreenBand : sessionPageActions}
actionsFill={fullscreen}
@@ -1075,6 +1076,7 @@
length of the move instead, uncovering it as it settles. Entering full screen needs
none of this: the band is full width from the first frame and the panel stays below. -->
<div
bind:clientWidth={contentWidth}
class="flex-1 min-h-0 flex flex-row relative {panesAnimating && !fullscreen
? 'z-[31]'
: 'z-0'}"
@@ -1083,6 +1085,7 @@
>
<Splitpanes
horizontal={false}
style="--panes-move: {PANES_MOVE_MS}ms"
class="flex-1 min-h-0 session-splitter {previewCollapsed || fullscreen
? 'splitter-off'
: ''} {panesAnimating ? 'panes-animating' : ''}"
@@ -1134,7 +1137,8 @@
bind:clientWidth={() => previewWidth,
(w) => {
previewWidth = w
if (!fullscreen && !panesAnimating) panelWidth = w
if (!fullscreen && !panesAnimating && contentWidth > 0)
panelShare = w / contentWidth
}}
class="flex-1 min-h-0 flex flex-col {fullscreen ? 'p-0 pt-11' : 'p-2 pl-0'}"
>
@@ -1321,9 +1325,9 @@
{/snippet}
{#snippet previewTabStrip(inBand: boolean)}
<!-- Both layouts render this one snippet, so the move between them is described once. The
wrapper exists to carry it: `stripArrive` measures this box against where the strip stood
before the switch. -->
<!-- Both layouts render this one snippet, so the strip is described once. The wrapper is here to
own its fade: the two strips stand in different places, and what carries the switch is the
panel moving under them, not the strip travelling. -->
<div
class={inBand ? 'flex items-center h-full min-w-0' : 'shrink-0'}
in:stripArrive
@@ -1450,9 +1454,10 @@
the panes' widths are animated — but only while that switch is in flight (`panes-animating`),
since a splitter drag sets the same inline width and would trail the pointer. */
:global(.splitpanes--vertical.panes-animating) > :global(.splitpanes__pane) {
/* `!important` to beat app.css's blanket `transition: none !important` on vertical panes,
/* The duration comes from PANES_MOVE_MS, which also times how long the class stays on.
`!important` to beat app.css's blanket `transition: none !important` on vertical panes,
which is there so a splitter drag tracks the pointer exactly. */
transition: width 240ms cubic-bezier(0.33, 1, 0.68, 1) !important;
transition: width var(--panes-move) cubic-bezier(0.33, 1, 0.68, 1) !important;
}
/* Collapsed preview: the pane is resized to 0 but stays mounted, so remove