Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 14 additions & 30 deletions src/browser/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@ import { EditCutoffBarrier } from "./Messages/ChatBarrier/EditCutoffBarrier";
import { StreamingBarrier } from "./Messages/ChatBarrier/StreamingBarrier";
import { RetryBarrier } from "./Messages/ChatBarrier/RetryBarrier";
import { PinnedTodoList } from "./PinnedTodoList";
import {
getAutoRetryKey,
VIM_ENABLED_KEY,
RIGHT_SIDEBAR_TAB_KEY,
} from "@/common/constants/storage";
import { getAutoRetryKey, VIM_ENABLED_KEY } from "@/common/constants/storage";
import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults";
import { ChatInput, type ChatInputAPI } from "./ChatInput/index";
import { RightSidebar, type TabType } from "./RightSidebar";
import { RightSidebar } from "./RightSidebar";
import { useResizableSidebar } from "@/browser/hooks/useResizableSidebar";
import {
shouldShowInterruptedBarrier,
Expand All @@ -38,7 +34,7 @@ import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
import { useAutoScroll } from "@/browser/hooks/useAutoScroll";
import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal";
import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor";
import { readPersistedState, usePersistedState } from "@/browser/hooks/usePersistedState";
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import { useThinking } from "@/browser/contexts/ThinkingContext";
import {
useWorkspaceState,
Expand Down Expand Up @@ -95,28 +91,17 @@ const AIViewInner: React.FC<AIViewProps> = ({
const { api } = useAPI();
const chatAreaRef = useRef<HTMLDivElement>(null);

// Track active tab to conditionally enable resize functionality
// Initialize from persisted value to avoid layout flash; RightSidebar owns the state
// and notifies us of changes via onTabChange callback
const [activeTab, setActiveTab] = useState<TabType>(() =>
readPersistedState<TabType>(RIGHT_SIDEBAR_TAB_KEY, "costs")
);

const isReviewTabActive = activeTab === "review";

// Resizable sidebar for Review tab only
// Hook encapsulates all drag logic, persistence, and constraints
// Returns width to apply to RightSidebar and startResize for handle's onMouseDown
// Resizable RightSidebar width (used for both Review + Costs to avoid tab-switch jank)
const {
width: sidebarWidth,
isResizing,
startResize,
} = useResizableSidebar({
enabled: isReviewTabActive, // Only active on Review tab
defaultWidth: 600, // Initial width or fallback
minWidth: 300, // Can't shrink smaller
maxWidth: 1200, // Can't grow larger
storageKey: "review-sidebar-width", // Persists across sessions
enabled: true,
defaultWidth: 300,
minWidth: 300,
maxWidth: 1200,
storageKey: "review-sidebar-width",
});

const workspaceState = useWorkspaceState(workspaceId);
Expand Down Expand Up @@ -775,12 +760,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
workspaceId={workspaceId}
workspacePath={namedWorkspacePath}
chatAreaRef={chatAreaRef}
onTabChange={setActiveTab} // Notifies us when tab changes
width={isReviewTabActive ? sidebarWidth : undefined} // Custom width only on Review tab
onStartResize={isReviewTabActive ? startResize : undefined} // Pass resize handler when Review active
isResizing={isResizing} // Pass resizing state
onReviewNote={handleReviewNote} // Pass review note handler to append to chat
isCreating={status === "creating"} // Workspace still being set up
width={sidebarWidth}
onStartResize={startResize}
isResizing={isResizing}
onReviewNote={handleReviewNote}
isCreating={status === "creating"}
/>

<PopoverError
Expand Down
24 changes: 14 additions & 10 deletions src/browser/components/RightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ interface RightSidebarProps {
workspaceId: string;
workspacePath: string;
chatAreaRef: React.RefObject<HTMLDivElement>;
/** Callback fired when tab selection changes (used for resize logic in AIView) */
onTabChange?: (tab: TabType) => void;
/** Custom width in pixels (overrides default widths when Review tab is resizable) */
width?: number;
/** Drag start handler for resize (Review tab only) */
Expand All @@ -101,7 +99,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
workspaceId,
workspacePath,
chatAreaRef,
onTabChange,
width,
onStartResize,
isResizing = false,
Expand All @@ -117,11 +114,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
// Review stats reported by ReviewPanel
const [reviewStats, setReviewStats] = React.useState<ReviewStats | null>(null);

// Notify parent (AIView) of tab changes so it can enable/disable resize functionality
React.useEffect(() => {
onTabChange?.(selectedTab);
}, [selectedTab, onTabChange]);

// Keyboard shortcuts for tab switching
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
Expand All @@ -137,7 +129,7 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({

window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [setSelectedTab, selectedTab]);
}, [setSelectedTab]);

const usage = useWorkspaceUsage(workspaceId);
const { options } = useProviderOptions();
Expand Down Expand Up @@ -212,14 +204,26 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
return;
}

// If the sidebar is custom-resized (wider than the default Costs width),
// auto-collapse based on chatAreaWidth can oscillate between expanded and
// collapsed states (because collapsed is 20px but expanded can be much wider),
// which looks like a constant flash. In that case, keep it expanded and let
// the user resize manually.
if (width !== undefined && width > 300) {
if (showCollapsed) {
setShowCollapsed(false);
}
return;
}

// Normal hysteresis for Costs/Tools tabs
if (chatAreaWidth <= COLLAPSE_THRESHOLD) {
setShowCollapsed(true);
} else if (chatAreaWidth >= EXPAND_THRESHOLD) {
setShowCollapsed(false);
}
// Between thresholds: maintain current state (no change)
}, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed]);
}, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed, width]);

// Single render point for VerticalTokenMeter
// Shows when: (1) collapsed, OR (2) Review tab is active
Expand Down