Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b275d8a
feat(web): add opt-in panel animations
maria-rcks Aug 30, 2026
3042663
fix(web): retain panels through exit animations
maria-rcks Aug 31, 2026
7842dff
fix(web): stabilize animated panels
maria-rcks Aug 31, 2026
08947f8
fix(web): keep panel resizing responsive
maria-rcks Aug 31, 2026
3db5612
fix(web): preserve sheet titlebar ownership
maria-rcks Aug 31, 2026
2666045
fix(web): restore controls during sheet exit
maria-rcks Aug 31, 2026
6f06e10
feat(web): add panel animation speed control
maria-rcks Aug 31, 2026
b1ce462
fix(web): clip animated panel contents
maria-rcks Aug 31, 2026
d16afce
fix(web): apply sidebar animation duration locally
maria-rcks Aug 31, 2026
db55dc5
fix(web): animate maximized panel close
maria-rcks Aug 31, 2026
5f9aa9a
fix(web): retain panel terminal shortcut target
maria-rcks Aug 31, 2026
6e2acb0
fix(web): retain right panel exit state
maria-rcks Aug 31, 2026
156b441
fix(web): skip transitions for panel clamps
maria-rcks Aug 31, 2026
b729de2
fix(web): hold panel clamp through paint
maria-rcks Aug 31, 2026
6e287e0
fix(web): synchronize panel header reserve
maria-rcks Aug 31, 2026
7d7896f
fix(web): cancel collapsed header gap
maria-rcks Aug 31, 2026
1af18dd
fix(web): preserve mobile drawer motion
maria-rcks Aug 31, 2026
8200766
feat(web): unify panel motion duration
maria-rcks Sep 1, 2026
dd63525
fix(web): preserve panel interaction semantics
maria-rcks Sep 1, 2026
1218d22
chore: resolve main conflicts
maria-rcks Sep 2, 2026
bd3041d
fix(web): preserve native controls during panel motion
maria-rcks Sep 2, 2026
8728a87
fix(web): refine panel motion settings layout
maria-rcks Sep 2, 2026
4b53946
fix(web): compact panel motion settings row
maria-rcks Sep 2, 2026
af9b69a
fix(web): wait for panel settings hydration
maria-rcks Sep 2, 2026
5ace05d
fix(web): restore disabled slider styling
maria-rcks Sep 2, 2026
2e99c2d
fix(web): compact panel animation setting
maria-rcks Sep 2, 2026
c3c5b7f
fix(web): smooth responsive panel motion
maria-rcks Sep 2, 2026
b43b45b
fix(web): harden responsive panel controls
maria-rcks Sep 2, 2026
68a55c6
refactor(web): simplify panel animations
maria-rcks Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const clientSettings: ClientSettings = {
fontSizeTerminal: 12,
fontSmoothing: true,
glassOpacity: 80,
panelAnimationsEnabled: false,
panelAnimationDurationMs: 200,
planModeEnabled: false,
showSkillsInSlashMenu: false,
providerModelPreferences: {},
Expand Down
13 changes: 12 additions & 1 deletion apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"
import { cn, isMacPlatform } from "../lib/utils";
import { primaryServerKeybindingsAtom } from "../state/server";
import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings";
import { usePanelAnimationSettings } from "../panelAnimations";
import LegacyThreadSidebar from "./LegacySidebar";
import ThreadSidebar from "./Sidebar";
import { SettingsSidebarNav } from "./settings/SettingsSidebarNav";
Expand Down Expand Up @@ -139,6 +140,8 @@ function ProjectProjectionRetention() {
export function AppSidebarLayout({ children }: { children: ReactNode }) {
const navigate = useNavigate();
const legacySidebarEnabled = useLegacySidebarEnabled();
const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
usePanelAnimationSettings();
// Settings routes show the settings nav in place of whichever thread
// sidebar is active.
const pathname = useLocation({ select: (location) => location.pathname });
Expand Down Expand Up @@ -166,6 +169,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
});
const sidebarProviderStyle = {
"--sidebar-width": `${sidebarWidth}px`,
"--panel-animation-duration": `${panelAnimationDurationMs}ms`,
...(isMacosDesktop && !isWindowFullscreen
? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET }
: {}),
Expand Down Expand Up @@ -209,9 +213,16 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
}, [navigate, pathname]);

return (
<SidebarProvider className="h-dvh! min-h-0!" defaultOpen style={sidebarProviderStyle}>
<SidebarProvider
className="h-dvh! min-h-0!"
data-panel-animations={panelAnimationsActive ? "true" : "false"}
defaultOpen
style={sidebarProviderStyle}
>
<ProjectProjectionRetention />
<Sidebar
animated={panelAnimationsActive}
animationDurationMs={panelAnimationDurationMs}
side="left"
collapsible="offcanvas"
data-app-sidebar=""
Expand Down
107 changes: 73 additions & 34 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ import {
useEnvironmentSettings,
} from "../hooks/useSettings";
import { useNowMinute } from "../hooks/useNowMinute";
import { usePanelAnimationSettings, usePanelPresence } from "../panelAnimations";
import { useNewThreadHandler } from "../hooks/useHandleNewThread";
import { useThreadActions } from "../hooks/useThreadActions";
import { resolveAppModelSelectionForInstance } from "../modelSelection";
Expand Down Expand Up @@ -1746,8 +1747,6 @@ function ChatViewContent(props: ChatViewProps) {
},
[activePullRequestSurfaceId],
);
const activeFileSurface =
activeRightPanelSurface?.kind === "file" ? activeRightPanelSurface : null;
const activePreviewState = useThreadPreviewState(activeThreadRef);
const activePreviewServerEpoch = activePreviewState.serverEpoch;
const resolvePreviewRuntimeTabId = useMemo(
Expand Down Expand Up @@ -1775,10 +1774,31 @@ function ChatViewContent(props: ChatViewProps) {
);
const previewPanelOpen = activeRightPanelKind === "preview" && isPreviewSupportedInRuntime();
const rightPanelOpen = rightPanelState.isOpen;
const canMaximizeRightPanel = rightPanelOpen && !shouldUseRightPanelSheet;
const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
usePanelAnimationSettings();
const inlineRightPanelPresence = usePanelPresence(
rightPanelOpen && !shouldUseRightPanelSheet && activeThreadRef !== null,
activeRightPanelSurface,
panelAnimationsActive && !shouldUseRightPanelSheet,
activeThreadKey,
panelAnimationDurationMs,
);
const sheetRightPanelPresence = usePanelPresence(
rightPanelOpen && shouldUseRightPanelSheet && activeThreadRef !== null,
activeRightPanelSurface,
panelAnimationsActive && shouldUseRightPanelSheet,
activeThreadKey,
panelAnimationDurationMs,
);
const inlineRightPanelPresent = inlineRightPanelPresence.present;
const sheetRightPanelPresent = sheetRightPanelPresence.present;
const rightPanelControlsInPanel =
inlineRightPanelPresent || (sheetRightPanelPresent && rightPanelOpen);
const renderedRightPanelSurface = inlineRightPanelPresence.value ?? sheetRightPanelPresence.value;
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
const canMaximizeRightPanel = inlineRightPanelPresent && !shouldUseRightPanelSheet;
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
const rightPanelMaximized =
canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey;
const inlineRightPanelOwnsTitleBar = rightPanelOpen && !shouldUseRightPanelSheet;
const inlineRightPanelOwnsTitleBar = inlineRightPanelPresent && !shouldUseRightPanelSheet;
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated

useEffect(() => {
if (!activeThreadRef) return;
Expand Down Expand Up @@ -7017,7 +7037,7 @@ function ChatViewContent(props: ChatViewProps) {
)}
data-workspace-titlebar-controls
>
{rightPanelOpen && !shouldUseRightPanelSheet ? (
{inlineRightPanelPresent && !shouldUseRightPanelSheet ? (
<RightPanelMaximizeControl
maximized={rightPanelMaximized}
onToggle={toggleRightPanelMaximized}
Expand All @@ -7027,23 +7047,23 @@ function ChatViewContent(props: ChatViewProps) {
</div>
);
const rightPanelContent = activeThreadRef ? (
activeRightPanelSurface?.kind === "preview" ? (
renderedRightPanelSurface?.kind === "preview" ? (
<Suspense fallback={null}>
<PreviewPanel
mode="embedded"
threadRef={activeThreadRef}
tabId={activeRightPanelSurface.resourceId}
tabId={renderedRightPanelSurface.resourceId}
configuredUrls={configuredPreviewUrls}
visible
onSendAnnotation={(annotation, image) => {
void onSend(undefined, "foreground", { annotation, image });
}}
/>
</Suspense>
) : activeRightPanelSurface?.kind === "terminal" ? (
) : renderedRightPanelSurface?.kind === "terminal" ? (
<PersistentThreadTerminalPanel
threadRef={activeThreadRef}
surface={activeRightPanelSurface}
surface={renderedRightPanelSurface}
launchContext={activeTerminalLaunchContext ?? null}
focusRequestId={terminalFocusRequestId}
keybindings={keybindings}
Expand All @@ -7058,7 +7078,7 @@ function ChatViewContent(props: ChatViewProps) {
newShortcutLabel={newTerminalShortcutLabel ?? undefined}
closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
/>
) : activeRightPanelSurface?.kind === "diff" ? (
) : renderedRightPanelSurface?.kind === "diff" ? (
<Suspense fallback={null}>
<DiffPanel
key={`${activeThreadKey}:${diffPanelGitStatusResolutionKey}`}
Expand All @@ -7068,26 +7088,26 @@ function ChatViewContent(props: ChatViewProps) {
workspaceMutationId={workspaceMutationId}
/>
</Suspense>
) : activeRightPanelSurface?.kind === "pull-request" && !pullRequestsCapabilityKnown ? (
) : renderedRightPanelSurface?.kind === "pull-request" && !pullRequestsCapabilityKnown ? (
<PullRequestDetailGhost />
) : activeRightPanelSurface?.kind === "pull-request" && !supportsPullRequests ? (
) : renderedRightPanelSurface?.kind === "pull-request" && !supportsPullRequests ? (
<PullRequestsUnavailableState
title="Pull requests unavailable"
error="Update this environment's T3 Code server to browse pull requests."
/>
) : activeRightPanelSurface?.kind === "pull-request" ? (
) : renderedRightPanelSurface?.kind === "pull-request" ? (
// No onClose: the surface tab's own X owns closing here, and a second X in the header
// would be the same action twice. The thread context also drops the checkout button, so it
// is only right for the thread's own pull request, whose branch is already under the
// reader's feet. A link the agent wrote can open any other one here, and that one has to be
// checkable out like it is anywhere else.
<PullRequestDetailPanel
key={`${activeRightPanelSurface.repository}#${activeRightPanelSurface.number}`}
key={`${renderedRightPanelSurface.repository}#${renderedRightPanelSurface.number}`}
environmentId={activeThread.environmentId}
reference={{
projectId: activeRightPanelSurface.projectId as ProjectId,
repository: activeRightPanelSurface.repository,
number: activeRightPanelSurface.number,
projectId: renderedRightPanelSurface.projectId as ProjectId,
repository: renderedRightPanelSurface.repository,
number: renderedRightPanelSurface.number,
}}
context={
isThreadOwnPullRequest(
Expand All @@ -7097,9 +7117,9 @@ function ChatViewContent(props: ChatViewProps) {
number: activeThreadPr?.number ?? null,
},
{
projectId: activeRightPanelSurface.projectId,
repository: activeRightPanelSurface.repository,
number: activeRightPanelSurface.number,
projectId: renderedRightPanelSurface.projectId,
repository: renderedRightPanelSurface.repository,
number: renderedRightPanelSurface.number,
},
)
? "thread"
Expand All @@ -7108,13 +7128,14 @@ function ChatViewContent(props: ChatViewProps) {
composerDraftTarget={composerDraftTarget}
onStateChange={handlePullRequestTabStatusChange}
/>
) : activeRightPanelSurface?.kind === "agents" ? (
) : renderedRightPanelSurface?.kind === "agents" ? (
<AgentsPanel
model={agentPanelModel}
environmentId={activeThreadRef?.environmentId ?? null}
threadId={activeThreadRef?.threadId ?? null}
/>
) : (activeRightPanelSurface?.kind === "files" || activeRightPanelSurface?.kind === "file") &&
) : (renderedRightPanelSurface?.kind === "files" ||
renderedRightPanelSurface?.kind === "file") &&
activeProject &&
activeWorkspaceRoot ? (
<Suspense fallback={null}>
Expand All @@ -7128,14 +7149,25 @@ function ChatViewContent(props: ChatViewProps) {
keybindings={keybindings}
availableEditors={availableEditors}
relativePath={
activeRightPanelSurface.kind === "file" ? activeRightPanelSurface.relativePath : null
renderedRightPanelSurface.kind === "file"
? renderedRightPanelSurface.relativePath
: null
}
revealLine={
renderedRightPanelSurface.kind === "file"
? (renderedRightPanelSurface.revealLine ?? null)
: null
}
revealRequestId={
renderedRightPanelSurface.kind === "file"
? renderedRightPanelSurface.revealRequestId
: 0
}
revealLine={activeFileSurface?.revealLine ?? null}
revealRequestId={activeFileSurface?.revealRequestId ?? 0}
onOpenFile={openFileSurface}
onPendingChange={handleFilePendingChange}
selectedFilePending={
activeFileSurface !== null && pendingFileSurfaceIds.has(activeFileSurface.id)
renderedRightPanelSurface.kind === "file" &&
pendingFileSurfaceIds.has(renderedRightPanelSurface.id)
}
workspaceMutationId={workspaceMutationId}
/>
Expand All @@ -7152,7 +7184,7 @@ function ChatViewContent(props: ChatViewProps) {

return (
<div className="relative flex min-h-0 min-w-0 flex-1 overflow-hidden bg-background">
{rightPanelOpen && !shouldUseRightPanelSheet ? panelLayoutControls : null}
{inlineRightPanelPresent && !shouldUseRightPanelSheet ? panelLayoutControls : null}
<div
className={cn(
"flex min-h-0 min-w-0 flex-col overflow-x-hidden",
Expand All @@ -7167,7 +7199,7 @@ function ChatViewContent(props: ChatViewProps) {
reserveNativeControls={reserveTitleBarControlInset && !inlineRightPanelOwnsTitleBar}
className="relative bg-background"
>
{!rightPanelOpen ? panelLayoutControls : null}
{!rightPanelControlsInPanel ? panelLayoutControls : null}
<ChatHeader
{...(!supportsPullRequests || activeProjectRepository === null
? {}
Expand All @@ -7188,7 +7220,7 @@ function ChatViewContent(props: ChatViewProps) {
}
keybindings={keybindings}
availableEditors={availableEditors}
rightPanelOpen={rightPanelOpen}
rightPanelOpen={rightPanelControlsInPanel}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
gitCwd={gitCwd}
onNewThreadInProject={handleNewThreadInActiveProject}
onRunProjectScript={runProjectScript}
Expand Down Expand Up @@ -7575,12 +7607,14 @@ function ChatViewContent(props: ChatViewProps) {
))}
</div>

{!shouldUseRightPanelSheet && rightPanelOpen && activeThreadRef ? (
{inlineRightPanelPresent && activeThreadRef ? (
<RightPanelTabs
mode="inline"
animated={panelAnimationsActive}
open={rightPanelOpen}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
maximized={rightPanelMaximized}
surfaces={rightPanelState.surfaces}
activeSurfaceId={activeRightPanelSurface?.id ?? null}
activeSurfaceId={renderedRightPanelSurface?.id ?? null}
pendingSurfaceIds={pendingFileSurfaceIds}
previewSessions={activePreviewState.sessions}
desktopByTabId={activePreviewState.desktopByTabId}
Expand Down Expand Up @@ -7610,8 +7644,13 @@ function ChatViewContent(props: ChatViewProps) {
{rightPanelContent}
</RightPanelTabs>
) : null}
{shouldUseRightPanelSheet && rightPanelOpen && activeThreadRef ? (
<RightPanelSheet open onClose={closePreviewPanel}>
{sheetRightPanelPresent && activeThreadRef ? (
<RightPanelSheet
animated={panelAnimationsActive}
animationDurationMs={panelAnimationDurationMs}
open={rightPanelOpen}
onClose={closePreviewPanel}
>
<RightPanelTabs
mode="sheet"
// Same effective inset as the closed-state titlebar controls
Expand All @@ -7620,7 +7659,7 @@ function ChatViewContent(props: ChatViewProps) {
// the sheet opens.
layoutControls={<div className="mr-px flex items-center">{panelToggleControls}</div>}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
surfaces={rightPanelState.surfaces}
activeSurfaceId={activeRightPanelSurface?.id ?? null}
activeSurfaceId={renderedRightPanelSurface?.id ?? null}
pendingSurfaceIds={pendingFileSurfaceIds}
previewSessions={activePreviewState.sessions}
desktopByTabId={activePreviewState.desktopByTabId}
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/RightPanelSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { RIGHT_PANEL_SHEET_CLASS_NAME } from "../rightPanelLayout";
import { Sheet, SheetPopup } from "./ui/sheet";

export function RightPanelSheet(props: {
animated: boolean;
animationDurationMs: number;
children: ReactNode;
open: boolean;
onClose: () => void;
Expand All @@ -18,6 +20,8 @@ export function RightPanelSheet(props: {
}}
>
<SheetPopup
animated={props.animated}
transitionDurationMs={props.animationDurationMs}
side="right"
showCloseButton={false}
keepMounted
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import { PierreEntryIcon } from "./chat/PierreEntryIcon";
interface RightPanelTabsProps {
mode: PreviewPanelMode;
maximized?: boolean;
animated?: boolean;
open?: boolean;
/** Forwarded to PreviewPanelShell so this surface persists its own width. */
widthStorageKey?: string;
/** Forwarded to PreviewPanelShell as the initial width before a user resize. */
Expand Down Expand Up @@ -773,6 +775,8 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
<PreviewPanelShell
mode={props.mode}
{...(props.maximized !== undefined ? { maximized: props.maximized } : {})}
{...(props.animated !== undefined ? { animated: props.animated } : {})}
{...(props.open !== undefined ? { open: props.open } : {})}
{...(props.widthStorageKey !== undefined ? { widthStorageKey: props.widthStorageKey } : {})}
{...(props.defaultWidth !== undefined ? { defaultWidth: props.defaultWidth } : {})}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/WorkspacePageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function WorkspacePageHeader({
return (
<header
className={cn(
"flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center gap-3 pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]",
"flex h-[var(--workspace-topbar-height)] min-h-[var(--workspace-topbar-height)] shrink-0 items-center gap-3 pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] [[data-panel-animations=true]_&]:motion-safe:transition-[padding-left] [[data-panel-animations=true]_&]:motion-safe:[transition-duration:var(--panel-animation-duration)] [[data-panel-animations=true]_&]:motion-safe:ease-out sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]",
electron && "drag-region",
reserveNativeControls && "wco:pr-[var(--workspace-native-controls-inset)]",
COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
Expand Down
Loading
Loading