From b0f2882659488ca00bb3423e6c1bb56931451acd Mon Sep 17 00:00:00 2001 From: "Leonardo R. Dias" <47978193+leoreisdias@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:26:01 -0300 Subject: [PATCH] Allow annotate terminal to dock on either side --- packages/editor/App.tsx | 177 +++++++++++++----- .../components/AnnotateAgentTerminalPanel.tsx | 38 +++- .../ui/utils/annotateAgentTerminal.test.ts | 16 +- packages/ui/utils/annotateAgentTerminal.ts | 15 ++ 4 files changed, 194 insertions(+), 52 deletions(-) diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 056126839..d07675f02 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -119,6 +119,11 @@ import { AnnotateAgentTerminalPanel, type AnnotateAgentTerminalPanelHandle, } from './components/AnnotateAgentTerminalPanel'; +import { + getSavedAnnotateAgentTerminalSide, + saveAnnotateAgentTerminalSide, + type AnnotateAgentTerminalSide, +} from '@plannotator/ui/utils/annotateAgentTerminal'; import { buildAgentTerminalDeliveryRecord, buildTerminalAskPrompt, @@ -165,6 +170,47 @@ type MessageAnnotationState = { selectedCodeAnnotationId: string | null; }; +type AgentTerminalLayoutOptions = { + showControls: boolean; + isOpen: boolean; + isRunning: boolean; + isWideMode: boolean; + isBelowBreakpoint: boolean; + side: AnnotateAgentTerminalSide; + isRightPanelOpen: boolean; +}; + +const getAgentTerminalLayout = ({ + showControls, + isOpen, + isRunning, + isWideMode, + isBelowBreakpoint, + side, + isRightPanelOpen, +}: AgentTerminalLayoutOptions) => { + const shouldRender = showControls && (isOpen || isRunning); + const isVisible = shouldRender && isOpen && !isWideMode && !isBelowBreakpoint; + const isLeft = side === 'left'; + const isLeftVisible = isVisible && isLeft; + const isRightVisible = isVisible && !isLeft; + const hiddenPositionClass = isLeft ? 'left-0' : 'right-0'; + const wrapperClassName = isVisible + ? 'flex h-full flex-shrink-0 group/agent-terminal' + : `absolute ${hiddenPositionClass} top-0 h-full w-0 overflow-hidden pointer-events-none group/agent-terminal`; + const directionClassName = isLeft ? 'flex-row' : 'flex-row-reverse'; + + return { + shouldRender, + isVisible, + isLeftVisible, + showOnLeft: shouldRender && isLeft, + showOnRight: shouldRender && !isLeft, + isRightPanelVisible: isRightPanelOpen && !isRightVisible, + dockClassName: `${wrapperClassName} ${directionClassName}`, + }; +}; + const countLinkedDocSessionAnnotations = (session: LinkedDocSessionState): number => { let total = session.root.annotations.length + @@ -399,6 +445,7 @@ const App: React.FC = () => { const [projectRoot, setProjectRoot] = useState(null); const [agentTerminalCapability, setAgentTerminalCapability] = useState(null); const [isAgentTerminalOpen, setIsAgentTerminalOpen] = useState(false); + const [agentTerminalSide, setAgentTerminalSide] = useState(getSavedAnnotateAgentTerminalSide); const [isAgentTerminalRunning, setIsAgentTerminalRunning] = useState(false); const [isAgentTerminalReady, setIsAgentTerminalReady] = useState(false); const [agentTerminalSessionId, setAgentTerminalSessionId] = useState(null); @@ -434,6 +481,7 @@ const App: React.FC = () => { const [showPlanAIAnnouncement, setShowPlanAIAnnouncement] = useState(needsPlanAIAnnouncement); const [showLookAndFeelAnnouncement, setShowLookAndFeelAnnouncement] = useState(needsLookAndFeelAnnouncement); const isMobile = useIsMobile(); + const isBelowAgentTerminalBreakpoint = useIsMobile(1024); const viewerRef = useRef(null); // containerRef + scrollViewport both point at the OverlayScrollbars @@ -477,10 +525,10 @@ const App: React.FC = () => { defaultWidth: 360, minWidth: 280, maxWidth: 640, - side: 'left', + side: agentTerminalSide, onSnapClose: () => setIsAgentTerminalOpen(false), // Single click on the handle (no drag) collapses it. - onClick: () => hideAgentTerminal(), + onClick: () => setIsAgentTerminalOpen(false), apply: (w) => document.documentElement.style.setProperty('--agent-terminal-w', `${w}px`), }); const isResizing = panelResize.isDragging || tocResize.isDragging || agentTerminalResize.isDragging; @@ -538,15 +586,29 @@ const App: React.FC = () => { sidebar.toggleTab(tab); }, [exitWideMode, wideModeType, sidebar.toggleTab]); + const hideAgentTerminal = useCallback(() => { + setIsAgentTerminalOpen(false); + }, []); + + const replaceRightAgentTerminalWithPanel = useCallback((tab: 'annotations' | 'ai') => { + hideAgentTerminal(); + setRightSidebarTab(tab); + setIsPanelOpen(true); + }, [hideAgentTerminal]); + const handleAnnotationPanelToggle = useCallback(() => { if (wideModeType !== null) { exitWideMode({ restore: false, panelOpen: true }); setRightSidebarTab('annotations'); return; } + if (agentTerminalSide === 'right' && isAgentTerminalOpen) { + replaceRightAgentTerminalWithPanel('annotations'); + return; + } setRightSidebarTab('annotations'); setIsPanelOpen(prev => rightSidebarTab === 'annotations' ? !prev : true); - }, [exitWideMode, rightSidebarTab, wideModeType]); + }, [agentTerminalSide, exitWideMode, isAgentTerminalOpen, replaceRightAgentTerminalWithPanel, rightSidebarTab, wideModeType]); const dismissPlanAIAnnouncement = useCallback(() => { markPlanAIAnnouncementSeen(); @@ -565,12 +627,17 @@ const App: React.FC = () => { setRightSidebarTab('ai'); return; } + if (agentTerminalSide === 'right' && isAgentTerminalOpen) { + replaceRightAgentTerminalWithPanel('ai'); + return; + } setRightSidebarTab('ai'); setIsPanelOpen(prev => rightSidebarTab === 'ai' ? !prev : true); - }, [dismissPlanAIAnnouncement, exitWideMode, rightSidebarTab, wideModeType]); + }, [agentTerminalSide, dismissPlanAIAnnouncement, exitWideMode, isAgentTerminalOpen, replaceRightAgentTerminalWithPanel, rightSidebarTab, wideModeType]); - const hideAgentTerminal = useCallback(() => { - setIsAgentTerminalOpen(false); + const handleAgentTerminalSideChange = useCallback((side: AnnotateAgentTerminalSide) => { + saveAnnotateAgentTerminalSide(side); + setAgentTerminalSide(side); }, []); const setAgentTerminalDelivery = useCallback((delivery: AgentTerminalDeliveryRecord | null) => { @@ -3778,11 +3845,52 @@ const App: React.FC = () => { annotateSource !== 'message' && agentTerminalCapability !== null && !goalSetupMode; - const shouldRenderAgentTerminal = - showAgentTerminalControls && - agentTerminalCapability !== null && - wideModeType === null && - (isAgentTerminalOpen || isAgentTerminalRunning); + const { + shouldRender: shouldRenderAgentTerminal, + isVisible: isAgentTerminalVisible, + isLeftVisible: isLeftAgentTerminalVisible, + showOnLeft: showAgentTerminalOnLeft, + showOnRight: showAgentTerminalOnRight, + isRightPanelVisible, + dockClassName: agentTerminalDockClassName, + } = getAgentTerminalLayout({ + showControls: showAgentTerminalControls, + isOpen: isAgentTerminalOpen, + isRunning: isAgentTerminalRunning, + isWideMode: wideModeType !== null, + isBelowBreakpoint: isBelowAgentTerminalBreakpoint, + side: agentTerminalSide, + isRightPanelOpen: isPanelOpen, + }); + const agentTerminalPanel = shouldRenderAgentTerminal && agentTerminalCapability ? ( +
+ + {isAgentTerminalVisible && ( + + )} +
+ ) : null; // Only greet in a normal authoring context — not on a read-only shared session // (a viewer would also be able to flip the owner's gridEnabled), nor over the // goal-setup / permission-mode flows. Deferred (not marked seen) until then. @@ -3832,9 +3940,9 @@ const App: React.FC = () => { origin={origin} isSubmitting={isSubmitting} isExiting={isExiting} - isPanelOpen={isPanelOpen && rightSidebarTab === 'annotations'} + isPanelOpen={isRightPanelVisible && rightSidebarTab === 'annotations'} aiAvailable={canUseAskAI} - isAIChatOpen={isPanelOpen && rightSidebarTab === 'ai'} + isAIChatOpen={isRightPanelVisible && rightSidebarTab === 'ai'} aiHasMessages={visibleAIMessages.length > 0} hasAnyAnnotations={hasAnyAnnotations || hasDirectEdits || hasSavedFileChanges} annotationCount={feedbackAnnotationCount} @@ -3948,38 +4056,9 @@ const App: React.FC = () => {
{/* Tater sprites — inside content wrapper so z-0 stacking context applies */} {taterMode && } - {shouldRenderAgentTerminal && agentTerminalCapability && ( -
- - {isAgentTerminalOpen && ( - - )} -
- )} + {showAgentTerminalOnLeft && agentTerminalPanel} {/* Left Sidebar: collapsed tab flags (when sidebar is closed) */} - {wideModeType === null && !sidebar.isOpen && !goalSetupMode && !isAgentTerminalOpen && ( + {wideModeType === null && !sidebar.isOpen && !goalSetupMode && !isLeftAgentTerminalVisible && ( { {/* Document Area */} @@ -4400,17 +4479,19 @@ const App: React.FC = () => {
+ {showAgentTerminalOnRight && agentTerminalPanel} + {/* Right panel region — `group/sidebar` so the collapse button reveals when hovering the whole panel, not just the thin handle. The handle and the panel(s) are separate sibling conditionals, so they need a shared hover ancestor (`contents` = no layout box). */}
{/* Resize Handle */} - {isPanelOpen && wideModeType === null && !goalSetupMode && (rightSidebarTab === 'annotations' || canUseAskAI) && setIsPanelOpen(false)} />} + {isRightPanelVisible && wideModeType === null && !goalSetupMode && (rightSidebarTab === 'annotations' || canUseAskAI) && setIsPanelOpen(false)} />} {/* Annotation Panel */} { })) ?? null} onOtherFileAnnotationsClick={handleFlashAnnotatedFiles} /> - {isPanelOpen && rightSidebarTab === 'ai' && wideModeType === null && !goalSetupMode && canUseAskAI && ( + {isRightPanelVisible && rightSidebarTab === 'ai' && wideModeType === null && !goalSetupMode && canUseAskAI && (