@@ -2,19 +2,15 @@ import React from "react";
22import { RIGHT_SIDEBAR_TAB_KEY , RIGHT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage" ;
33import { usePersistedState } from "@/browser/hooks/usePersistedState" ;
44import { useWorkspaceUsage , useWorkspaceStatsSnapshot } from "@/browser/stores/WorkspaceStore" ;
5- import { useProviderOptions } from "@/browser/hooks/useProviderOptions" ;
6- import { useResizeObserver } from "@/browser/hooks/useResizeObserver" ;
75import { useFeatureFlags } from "@/browser/contexts/FeatureFlagsContext" ;
8- import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings" ;
96import { ErrorBoundary } from "./ErrorBoundary" ;
107import { CostsTab } from "./RightSidebar/CostsTab" ;
118import { StatsTab } from "./RightSidebar/StatsTab" ;
12- import { VerticalTokenMeter } from "./RightSidebar/VerticalTokenMeter" ;
139import { ReviewPanel } from "./RightSidebar/CodeReview/ReviewPanel" ;
14- import { calculateTokenMeterData } from "@/common/utils/tokens/tokenMeterUtils" ;
1510import { sumUsageHistory , type ChatUsageDisplay } from "@/common/utils/tokens/usageAggregator" ;
1611import { matchesKeybind , KEYBINDS , formatKeybind } from "@/browser/utils/ui/keybinds" ;
1712import { Tooltip , TooltipTrigger , TooltipContent } from "./ui/tooltip" ;
13+ import { SidebarCollapseButton } from "./ui/SidebarCollapseButton" ;
1814import { cn } from "@/common/lib/utils" ;
1915import type { ReviewNoteData } from "@/common/types/review" ;
2016
@@ -34,7 +30,7 @@ function formatTabDuration(ms: number): string {
3430}
3531
3632interface SidebarContainerProps {
37- collapsed : boolean ;
33+ collapsed ? : boolean ;
3834 wide ?: boolean ;
3935 /** Custom width from drag-resize (persisted per-tab by AIView) */
4036 customWidth ?: number ;
@@ -49,7 +45,7 @@ interface SidebarContainerProps {
4945 * SidebarContainer - Main sidebar wrapper with dynamic width
5046 *
5147 * Width priority (first match wins):
52- * 1. collapsed (20px) - Shows vertical token meter only
48+ * 1. collapsed (20px) - Manual collapse via toggle
5349 * 2. customWidth - From drag-resize (persisted per-tab)
5450 * 3. wide - Auto-calculated max width for Review tab (when not drag-resizing)
5551 * 4. default (300px) - Costs tab when no customWidth saved
@@ -64,7 +60,7 @@ const SidebarContainer: React.FC<SidebarContainerProps> = ({
6460 "aria-label" : ariaLabel ,
6561} ) => {
6662 const width = collapsed
67- ? "20px"
63+ ? "20px" // Match left sidebar collapsed width (w-5 = 20px)
6864 : customWidth
6965 ? `${ customWidth } px`
7066 : wide
@@ -74,10 +70,8 @@ const SidebarContainer: React.FC<SidebarContainerProps> = ({
7470 return (
7571 < div
7672 className = { cn (
77- "bg-sidebar border-l border-border-light flex flex-col overflow-hidden flex-shrink-0" ,
73+ "bg-sidebar border-l border-border-light relative flex flex-col overflow-hidden flex-shrink-0" ,
7874 ! isResizing && "transition-[width] duration-200" ,
79- collapsed && "sticky right-0 z-10 shadow-[-2px_0_4px_rgba(0,0,0,0.2)]" ,
80- // Mobile: Show vertical meter when collapsed (20px), full width when expanded
8175 "max-md:border-l-0 max-md:border-t max-md:border-border-light" ,
8276 ! collapsed && "max-md:w-full max-md:relative max-md:max-h-[50vh]"
8377 ) }
@@ -97,7 +91,6 @@ export type { TabType };
9791interface RightSidebarProps {
9892 workspaceId : string ;
9993 workspacePath : string ;
100- chatAreaRef : React . RefObject < HTMLDivElement > ;
10194 /** Custom width in pixels (persisted per-tab, provided by AIView) */
10295 width ?: number ;
10396 /** Drag start handler for resize */
@@ -113,7 +106,6 @@ interface RightSidebarProps {
113106const RightSidebarComponent : React . FC < RightSidebarProps > = ( {
114107 workspaceId,
115108 workspacePath,
116- chatAreaRef,
117109 width,
118110 onStartResize,
119111 isResizing = false ,
@@ -123,6 +115,9 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
123115 // Global tab preference (not per-workspace)
124116 const [ selectedTab , setSelectedTab ] = usePersistedState < TabType > ( RIGHT_SIDEBAR_TAB_KEY , "costs" ) ;
125117
118+ // Manual collapse state (persisted globally)
119+ const [ collapsed , setCollapsed ] = usePersistedState < boolean > ( RIGHT_SIDEBAR_COLLAPSED_KEY , false ) ;
120+
126121 const { statsTabState } = useFeatureFlags ( ) ;
127122 const statsTabEnabled = Boolean ( statsTabState ?. enabled ) ;
128123
@@ -160,10 +155,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
160155
161156 const usage = useWorkspaceUsage ( workspaceId ) ;
162157
163- const { options } = useProviderOptions ( ) ;
164- const use1M = options . anthropic ?. use1MContext ?? false ;
165- const chatAreaSize = useResizeObserver ( chatAreaRef ) ;
166-
167158 const baseId = `right-sidebar-${ workspaceId } ` ;
168159 const costsTabId = `${ baseId } -tab-costs` ;
169160 const statsTabId = `${ baseId } -tab-stats` ;
@@ -172,10 +163,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
172163 const statsPanelId = `${ baseId } -panel-stats` ;
173164 const reviewPanelId = `${ baseId } -panel-review` ;
174165
175- // Use lastContextUsage for context window display (last step = actual context size)
176- const lastUsage = usage ?. liveUsage ?? usage ?. lastContextUsage ;
177- const model = lastUsage ?. model ?? null ;
178-
179166 // Calculate session cost for tab display
180167 const sessionCost = React . useMemo ( ( ) => {
181168 const parts : ChatUsageDisplay [ ] = [ ] ;
@@ -206,109 +193,30 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
206193 return total > 0 ? total : null ;
207194 } ) ( ) ;
208195
209- // Auto-compaction settings: threshold per-model
210- const { threshold : autoCompactThreshold , setThreshold : setAutoCompactThreshold } =
211- useAutoCompactionSettings ( workspaceId , model ) ;
212-
213- // Memoize vertical meter data calculation to prevent unnecessary re-renders
214- const verticalMeterData = React . useMemo ( ( ) => {
215- return lastUsage
216- ? calculateTokenMeterData ( lastUsage , model ?? "unknown" , use1M , true )
217- : { segments : [ ] , totalTokens : 0 , totalPercentage : 0 } ;
218- } , [ lastUsage , model , use1M ] ) ;
219-
220- // Calculate if we should show collapsed view with hysteresis
221- // Strategy: Observe ChatArea width directly (independent of sidebar width)
222- // - ChatArea has min-width: 750px and flex: 1
223- // - Use hysteresis to prevent oscillation:
224- // * Collapse when chatAreaWidth <= 800px (tight space)
225- // * Expand when chatAreaWidth >= 1100px (lots of space)
226- // * Between 800-1100: maintain current state (dead zone)
227- const COLLAPSE_THRESHOLD = 800 ; // Collapse below this
228- const EXPAND_THRESHOLD = 1100 ; // Expand above this
229- const chatAreaWidth = chatAreaSize ?. width ?? 1000 ; // Default to large to avoid flash
230-
231- // Persist collapsed state globally (not per-workspace) since chat area width is shared
232- // This prevents animation flash when switching workspaces - sidebar maintains its state
233- const [ showCollapsed , setShowCollapsed ] = usePersistedState < boolean > (
234- RIGHT_SIDEBAR_COLLAPSED_KEY ,
235- false
236- ) ;
237-
238- React . useEffect ( ( ) => {
239- // Never collapse when Review tab is active - code review needs space
240- if ( selectedTab === "review" ) {
241- if ( showCollapsed ) {
242- setShowCollapsed ( false ) ;
243- }
244- return ;
245- }
246-
247- // If the sidebar is custom-resized (wider than the default Costs width),
248- // auto-collapse based on chatAreaWidth can oscillate between expanded and
249- // collapsed states (because collapsed is 20px but expanded can be much wider),
250- // which looks like a constant flash. In that case, keep it expanded and let
251- // the user resize manually.
252- if ( width !== undefined && width > 300 ) {
253- if ( showCollapsed ) {
254- setShowCollapsed ( false ) ;
255- }
256- return ;
257- }
258-
259- // Normal hysteresis for Costs/Tools tabs
260- if ( chatAreaWidth <= COLLAPSE_THRESHOLD ) {
261- setShowCollapsed ( true ) ;
262- } else if ( chatAreaWidth >= EXPAND_THRESHOLD ) {
263- setShowCollapsed ( false ) ;
264- }
265- // Between thresholds: maintain current state (no change)
266- } , [ chatAreaWidth , selectedTab , showCollapsed , setShowCollapsed , width ] ) ;
267-
268- // Single render point for VerticalTokenMeter
269- // Shows when: (1) collapsed, OR (2) Review tab is active
270- const showMeter = showCollapsed || selectedTab === "review" ;
271- const autoCompactionProps = React . useMemo (
272- ( ) => ( {
273- threshold : autoCompactThreshold ,
274- setThreshold : setAutoCompactThreshold ,
275- } ) ,
276- [ autoCompactThreshold , setAutoCompactThreshold ]
277- ) ;
278- const verticalMeter = showMeter ? (
279- < VerticalTokenMeter data = { verticalMeterData } autoCompaction = { autoCompactionProps } />
280- ) : null ;
281-
282196 return (
283197 < SidebarContainer
284- collapsed = { showCollapsed }
198+ collapsed = { collapsed }
285199 wide = { selectedTab === "review" && ! width } // Auto-wide only if not drag-resizing
286200 customWidth = { width } // Per-tab resized width from AIView
287201 isResizing = { isResizing }
288202 role = "complementary"
289203 aria-label = "Workspace insights"
290204 >
291- { /* Full view when not collapsed */ }
292- < div className = { cn ( "flex-row h-full" , ! showCollapsed ? "flex" : "hidden" ) } >
293- { /* Resize handle (left edge) */ }
294- { onStartResize && (
295- < div
296- className = { cn (
297- "w-0.5 flex-shrink-0 z-10 transition-[background] duration-150 cursor-col-resize",
298- isResizing ? "bg-accent" : "bg-border-light hover:bg-accent"
299- ) }
300- onMouseDown = { ( e ) => onStartResize ( e as unknown as React . MouseEvent ) }
301- />
302- ) }
205+ { ! collapsed && (
206+ < >
207+ { /* Resize handle (left edge) */ }
208+ { onStartResize && (
209+ < div
210+ className = { cn (
211+ "absolute left-0 top-0 bottom-0 w-0.5 z-10 transition-[background] duration-150 cursor-col-resize",
212+ isResizing ? "bg-accent" : "bg-border-light hover:bg-accent"
213+ ) }
214+ onMouseDown = { ( e ) => onStartResize ( e as unknown as React . MouseEvent ) }
215+ />
216+ ) }
303217
304- { /* Render meter when Review tab is active */ }
305- { selectedTab === "review" && (
306- < div className = "bg-sidebar flex w-5 shrink-0 flex-col" > { verticalMeter } </ div >
307- ) }
308-
309- < div className = "flex min-w-0 flex-1 flex-col" >
310218 < div
311- className = "border-border-light flex gap-1 border-b px-2 py-1.5 "
219+ className = "border-border-light flex h-8 shrink-0 items-center gap-1 border-b px-2"
312220 role = "tablist"
313221 aria-label = "Metadata views"
314222 >
@@ -440,10 +348,14 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
440348 </ div >
441349 ) }
442350 </ div >
443- </ div >
444- </ div >
445- { /* Render meter in collapsed view when sidebar is collapsed */ }
446- < div className = { cn ( "h-full" , showCollapsed ? "flex" : "hidden" ) } > { verticalMeter } </ div >
351+ </ >
352+ ) }
353+
354+ < SidebarCollapseButton
355+ collapsed = { collapsed }
356+ onToggle = { ( ) => setCollapsed ( ! collapsed ) }
357+ side = "right"
358+ />
447359 </ SidebarContainer >
448360 ) ;
449361} ;
0 commit comments