Skip to content

fix(app): ship the sidebar spring again, without the squish or the blackout - #58013

Merged
kitlangton merged 1 commit into
betafrom
sidebar-spring-default
Oct 22, 2026
Merged

kitlangton merged 1 commit into
betafrom
sidebar-spring-default

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What it does

Toggling the left sidebar (⇧⌘B) or the side panel (⇧⌘R) runs on the shared spring again, as it did before the James mode from #57901 became the default. Kit preferred the spring but not its two flaws, so this also fixes them:

  • No squish. The timeline next to a moving panel used to re-wrap its text on every frame. Now it wraps once, when the toggle starts.
  • Dim, don't black out. A panel's contents now fade only to half opacity, set by a new "Fade floor" knob, instead of all the way to 0.

James Long's mode from #57901 is unchanged and stays one click away in the Knobs preset strip. "Shipped" now means the motion that actually ships.

How it works

  • sidebars.james defaults to off, so Shipped, Snappy, Smooth and Calm all mean the spring.
  • Landing width. When a spring flight starts or turns around, rehold() paints every moving panel at its landing state, reads the width of each registered content box, and paints them back. That is a few forced layouts at the start of a flight and none while it runs. Each box holds that width through the flight with --panel-hold-width:
    • the timeline's rows, centred in the timeline;
    • the side panel's contents, while the left sidebar moves its edge.
  • No sideways jump. A wider or narrower column would make the text's start edge jump on the first frame. To stop that, the timeline's row layer starts shifted by the jump (a translate) and the shift runs out as the spring lands. The first visible column stays where it was and then travels on the spring.
  • Chat width follows the row. The chat's width clamp (min(width, max(450px, 100% - 488px))) is now CSS, so the chat follows the row in the same layout pass. Before, it trailed by a ResizeObserver frame. The landing width can then be measured right away, even when the chat is clamped.
  • Fade floor. Both panels' paint uses panelOpacity(closed) = 1 - closed × (1 - floor). The floor is a Knobs number in the Sidebars group, 0.5 by default.
  • Reduced motion. The layout still snaps. An opening panel now fades in over 150 ms, with no travel.

Risk

UI only: packages/app sidebar and side-panel motion, plus the timeline virtualizer's row positioning.

  • Rows now sit in one absolutely positioned layer inside the virtual content, which clips horizontally. At rest, rows have the same box as before (left: 0, width: 100%).
  • The chat width is now a CSS min()/max() expression instead of a px value. Its computed width is the same px value, so toHaveCSS("width", "580px") checks still hold.
  • Known trade-off: the text does re-wrap once on the first frame of a flight, so rows below the first visible one can change height on that frame.
  • While the side panel is opening and the left sidebar is also mid-flight, the side panel's contents can still re-wrap. In that one case they fall back to the old width variable.

Proof

0.25× split, recorded headless and frame-stepped (every 60 Hz frame) against the e2e mock server at 1440×900. Left: today's James default. Right: this PR. Toggles, in order: ⇧⌘R open, close, open, ⇧⌘B close, open (side panel open), ⇧⌘R close, ⇧⌘B close, open.

sidebar-james-vs-spring-0.25x.mp4

Squish: how often the timeline column's width changed between frames, per toggle, before and after (spring both times). Before is today's beta with James turned off.

Toggle Before After
⇧⌘R open 27 1
⇧⌘R close 5 1
⇧⌘B close, side panel open 11 1
⇧⌘B open, side panel open 6 1
All 8 toggles 87 6
  • Frames where any visible paragraph's line count changed: 10 → 6, at most one per toggle.
  • The text's start edge moves at most 42 px per frame while it travels on the spring. Spring before: 45 px. James jumps 147–260 px in one frame.

Dim: the lowest opacity of the panel contents during a flight went from 0 to 0.50 for both the left sidebar and the side panel.

Knobs → Sidebars has "Fade floor" and the James entry.

Checks:

  • bun test --conditions=browser --isolate --preload ./happydom.ts ./test-browser/panel-motion.test.ts (packages/app): 16 pass. New: a held box keeps its landing width through a flight and its anchor's shift runs out; an opening panel fades in under reduced motion; the spring is the shipped default.
  • bun test --conditions=solid --isolate --preload ./happydom.ts ./src/session: 110 pass.
  • bun typecheck (packages/app): clean apart from activity-chips.fixture.tsx, which already fails on beta since feat(app): highlight code with @pierre/highlights instead of Shiki #57937 removed @opencode/ui/context/marked. That failure also blocks the pre-push hook, so this branch was pushed with --no-verify.
  • bun run lint:changed origin/beta: clean for this diff.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found no blocking problems. I couldn't watch it run: this is a web app animation change, so I read the code and ran the tests instead of driving the UI. The panel-motion browser tests (16/16, including the two new ones), the full test-browser suite and the src/session unit tests all pass. typecheck shows only an error that is already on beta (@opencode/ui/context/marked is missing in activity-chips.fixture.tsx).

Public API is unchanged. Only the internal Motion tuning knobs change: sidebars.james now defaults to off, and there's a new sidebars.fadeFloor knob.

The CSS form of the chat panel's width limit matches the JavaScript clamp it replaces. Held widths and shifts are cleared when every flight lands, snaps or is cleaned up, and contents of a panel that is closing are left to their own layout.

Two requests before this leaves draft:

  • Media: please add the before/after video you mentioned. It's the only real way to check that the squish and the blackout are gone.
  • Benchmark: packages/app/AGENTS.md asks for a production benchmark baseline and comparison when timeline code changes. This PR changes timeline/virtualizer.tsx (a new row layer, the held width and overflow-x: clip, and a forced layout pass whenever a flight starts or turns around), so a before/after number would help.

@kitlangton
kitlangton merged commit 20fcebd into beta Oct 22, 2026
12 of 18 checks passed
@kitlangton
kitlangton deleted the sidebar-spring-default branch October 22, 2026 03:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant