From 83feaa58179c5acc375e134066f1dff17139879e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 18 May 2026 12:53:46 -0700 Subject: [PATCH] Smooth-open onboarding skill setup terminal (#2262) Co-authored-by: Orca --- .../onboarding/FeatureSetupInlineTerminal.tsx | 129 ++++++++++++------ 1 file changed, 88 insertions(+), 41 deletions(-) diff --git a/src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.tsx b/src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.tsx index ff93a1e90..116cf7c3f 100644 --- a/src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.tsx +++ b/src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.tsx @@ -28,8 +28,19 @@ export function FeatureSetupInlineTerminal({ const closeTab = useAppStore((s) => s.closeTab) const setActiveTabForWorktree = useAppStore((s) => s.setActiveTabForWorktree) const setTabCustomTitle = useAppStore((s) => s.setTabCustomTitle) + const prefersReducedMotion = useMemo( + () => + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + [] + ) const [cwd, setCwd] = useState(null) const [tabId, setTabId] = useState(null) + // Why: starts at `prefersReducedMotion` so users opted out of motion never + // see the slide-in frame; otherwise we flip to true after first paint so the + // CSS transition has a starting state to interpolate from. + const [entered, setEntered] = useState(prefersReducedMotion) const terminalSectionRef = useRef(null) const autoInsertedRef = useRef(null) const terminalOpenedTrackedRef = useRef(false) @@ -83,17 +94,39 @@ export function FeatureSetupInlineTerminal({ }, [createTab, setActiveTabForWorktree, setTabCustomTitle]) useEffect(() => { - const frame = window.requestAnimationFrame(() => { - const prefersReducedMotion = - typeof window.matchMedia === 'function' && - window.matchMedia('(prefers-reduced-motion: reduce)').matches - terminalSectionRef.current?.scrollIntoView({ - behavior: prefersReducedMotion ? 'auto' : 'smooth', - block: 'center' + if (prefersReducedMotion) { + const scrollFrame = window.requestAnimationFrame(() => { + terminalSectionRef.current?.scrollIntoView({ behavior: 'auto', block: 'center' }) }) + return () => window.cancelAnimationFrame(scrollFrame) + } + // Why: double rAF guarantees the browser commits the initial collapsed + // styles before we flip to `entered`, so the height/opacity transition + // actually plays instead of snapping straight to the final state. + const enterFrame = window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => setEntered(true)) }) - return () => window.cancelAnimationFrame(frame) - }, []) + return () => window.cancelAnimationFrame(enterFrame) + }, [prefersReducedMotion]) + + // Why: tracking scroll *during* the height transition is unavoidably + // jumpy — ResizeObserver / rAF ticks land in pixel-sized chunks, and each + // chunk reads as a step. Instead, let the section grow in place, then once + // the height has nearly settled fire a single native smooth scroll. The + // browser eases that scroll itself, which is the smoothest path available. + useEffect(() => { + if (!entered || prefersReducedMotion) { + return + } + const section = terminalSectionRef.current + if (!section) { + return + } + const scrollTimer = window.setTimeout(() => { + section.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, 500) + return () => window.clearTimeout(scrollTimer) + }, [entered, prefersReducedMotion]) const insertCommand = useCallback(() => { if (!tabId) { @@ -148,41 +181,55 @@ export function FeatureSetupInlineTerminal({ } }, [command, insertCommand, tabId]) + // Why: grid 0fr → 1fr animates to the child's natural height without a + // hardcoded max-height, so we don't leave dead space if the terminal + // section's intrinsic size shifts. The inner section is positioned via the + // grid row, so xterm.js measures its real container on mount. return ( -
-
-

- Press Enter to run the command and confirm npm if asked. You can also set this up later in - Settings. -

-
-
trackTerminalInteraction('keyboard', event)} - onPointerDownCapture={() => trackTerminalInteraction('pointer')} +
- {cwd && tabId ? ( - closeTab(tabId)} - onCloseTab={() => closeTab(tabId)} - /> - ) : ( -
- - Starting terminal... -
- )} -
-
+
+

+ Press Enter to run the command and confirm npm if asked. You can also set this up later + in Settings. +

+
+
trackTerminalInteraction('keyboard', event)} + onPointerDownCapture={() => trackTerminalInteraction('pointer')} + > + {cwd && tabId ? ( + closeTab(tabId)} + onCloseTab={() => closeTab(tabId)} + /> + ) : ( +
+ + Starting terminal... +
+ )} +
+ + ) }