From 8353e0a69f8cc248b0eeb7226cdb1128ae4c647e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:48:02 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20header=20conte?= =?UTF-8?q?xt=20usage=20indicator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/components/ChatInput/index.tsx | 30 ++++ .../ContextUsageIndicatorButton.tsx | 57 ++++++ .../RightSidebar/ContextUsageBar.tsx | 57 ++++++ .../components/RightSidebar/CostsTab.tsx | 165 ++---------------- .../components/RightSidebar/TokenMeter.tsx | 5 +- 5 files changed, 165 insertions(+), 149 deletions(-) create mode 100644 src/browser/components/ContextUsageIndicatorButton.tsx create mode 100644 src/browser/components/RightSidebar/ContextUsageBar.tsx diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx index 618740d22b..62f8145e16 100644 --- a/src/browser/components/ChatInput/index.tsx +++ b/src/browser/components/ChatInput/index.tsx @@ -46,6 +46,11 @@ import { } from "@/browser/utils/slashCommands/suggestions"; import { Tooltip, TooltipTrigger, TooltipContent, HelpIndicator } from "../ui/tooltip"; import { ModeSelector } from "../ModeSelector"; +import { ContextUsageIndicatorButton } from "../ContextUsageIndicatorButton"; +import { useWorkspaceUsage } from "@/browser/stores/WorkspaceStore"; +import { useProviderOptions } from "@/browser/hooks/useProviderOptions"; +import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings"; +import { calculateTokenMeterData } from "@/common/utils/tokens/tokenMeterUtils"; import { matchesKeybind, formatKeybind, @@ -246,6 +251,25 @@ const ChatInputInner: React.FC = (props) => { const preferredModel = sendMessageOptions.model; const baseModel = sendMessageOptions.baseModel; + // Context usage indicator data (workspace variant only) + const workspaceIdForUsage = variant === "workspace" ? props.workspaceId : ""; + const usage = useWorkspaceUsage(workspaceIdForUsage); + const { options: providerOptions } = useProviderOptions(); + const use1M = providerOptions.anthropic?.use1MContext ?? false; + const lastUsage = usage?.liveUsage ?? usage?.lastContextUsage; + const usageModel = lastUsage?.model ?? null; + const contextUsageData = useMemo(() => { + return lastUsage + ? calculateTokenMeterData(lastUsage, usageModel ?? "unknown", use1M, false) + : { segments: [], totalTokens: 0, totalPercentage: 0 }; + }, [lastUsage, usageModel, use1M]); + const { threshold: autoCompactThreshold, setThreshold: setAutoCompactThreshold } = + useAutoCompactionSettings(workspaceIdForUsage, usageModel); + const autoCompactionProps = useMemo( + () => ({ threshold: autoCompactThreshold, setThreshold: setAutoCompactThreshold }), + [autoCompactThreshold, setAutoCompactThreshold] + ); + const setPreferredModel = useCallback( (model: string) => { ensureModelInSettings(model); // Ensure model exists in Settings @@ -1638,6 +1662,12 @@ const ChatInputInner: React.FC = (props) => { data-component="ModelControls" data-tutorial="mode-selector" > + {variant === "workspace" && ( + + )} diff --git a/src/browser/components/ContextUsageIndicatorButton.tsx b/src/browser/components/ContextUsageIndicatorButton.tsx new file mode 100644 index 0000000000..05650b7ca2 --- /dev/null +++ b/src/browser/components/ContextUsageIndicatorButton.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover"; +import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip"; +import { ContextUsageBar } from "./RightSidebar/ContextUsageBar"; +import { TokenMeter } from "./RightSidebar/TokenMeter"; +import type { AutoCompactionConfig } from "./RightSidebar/ThresholdSlider"; +import { formatTokens, type TokenMeterData } from "@/common/utils/tokens/tokenMeterUtils"; + +interface ContextUsageIndicatorButtonProps { + data: TokenMeterData; + autoCompaction?: AutoCompactionConfig; +} + +export const ContextUsageIndicatorButton: React.FC = ({ + data, + autoCompaction, +}) => { + const [popoverOpen, setPopoverOpen] = React.useState(false); + + if (data.totalTokens === 0) return null; + + const ariaLabel = data.maxTokens + ? `Context usage: ${formatTokens(data.totalTokens)} / ${formatTokens(data.maxTokens)} (${data.totalPercentage.toFixed( + 1 + )}%)` + : `Context usage: ${formatTokens(data.totalTokens)} (unknown limit)`; + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/browser/components/RightSidebar/ContextUsageBar.tsx b/src/browser/components/RightSidebar/ContextUsageBar.tsx new file mode 100644 index 0000000000..63c987dbf0 --- /dev/null +++ b/src/browser/components/RightSidebar/ContextUsageBar.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { TokenMeter } from "./TokenMeter"; +import { HorizontalThresholdSlider, type AutoCompactionConfig } from "./ThresholdSlider"; +import { formatTokens, type TokenMeterData } from "@/common/utils/tokens/tokenMeterUtils"; + +interface ContextUsageBarProps { + data: TokenMeterData; + /** Auto-compaction settings for threshold slider */ + autoCompaction?: AutoCompactionConfig; + showTitle?: boolean; + testId?: string; +} + +const ContextUsageBarComponent: React.FC = ({ + data, + autoCompaction, + showTitle = true, + testId, +}) => { + if (data.totalTokens === 0) return null; + + const totalDisplay = formatTokens(data.totalTokens); + const maxDisplay = data.maxTokens ? ` / ${formatTokens(data.maxTokens)}` : ""; + const percentageDisplay = data.maxTokens ? ` (${data.totalPercentage.toFixed(1)}%)` : ""; + + const showWarning = !data.maxTokens; + + return ( +
+
+ {showTitle && ( + + Context Usage + + )} + + {totalDisplay} + {maxDisplay} + {percentageDisplay} + +
+ +
+ + {autoCompaction && data.maxTokens && } +
+ + {showWarning && ( +
+ Unknown model limits - showing relative usage only +
+ )} +
+ ); +}; + +export const ContextUsageBar = React.memo(ContextUsageBarComponent); diff --git a/src/browser/components/RightSidebar/CostsTab.tsx b/src/browser/components/RightSidebar/CostsTab.tsx index ea88455f59..91e3d25f83 100644 --- a/src/browser/components/RightSidebar/CostsTab.tsx +++ b/src/browser/components/RightSidebar/CostsTab.tsx @@ -6,9 +6,13 @@ import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { ToggleGroup, type ToggleOption } from "../ToggleGroup"; import { useProviderOptions } from "@/browser/hooks/useProviderOptions"; import { supports1MContext } from "@/common/utils/ai/models"; -import { TOKEN_COMPONENT_COLORS } from "@/common/utils/tokens/tokenMeterUtils"; +import { + TOKEN_COMPONENT_COLORS, + calculateTokenMeterData, + formatTokens, +} from "@/common/utils/tokens/tokenMeterUtils"; import { ConsumerBreakdown } from "./ConsumerBreakdown"; -import { HorizontalThresholdSlider } from "./ThresholdSlider"; +import { ContextUsageBar } from "./ContextUsageBar"; import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings"; import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip"; import { PostCompactionSection } from "./PostCompactionSection"; @@ -16,10 +20,6 @@ import { usePostCompactionState } from "@/browser/hooks/usePostCompactionState"; import { useExperimentValue } from "@/browser/contexts/ExperimentsContext"; import { EXPERIMENT_IDS } from "@/common/constants/experiments"; -// Format token display - show k for thousands with 1 decimal -const formatTokens = (tokens: number) => - tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : tokens.toLocaleString(); - // Format cost display - show "??" if undefined, "<$0.01" for very small values, otherwise fixed precision const formatCost = (cost: number | undefined): string => { if (cost === undefined) return "??"; @@ -123,153 +123,22 @@ const CostsTabComponent: React.FC = ({ workspaceId }) => {
{(() => { - // Context usage: live when streaming, else last historical - // Uses lastContextUsage (last step) for accurate context window size const contextUsage = usage.liveUsage ?? usage.lastContextUsage; const model = contextUsage?.model ?? "unknown"; - // Get max tokens for the model from the model stats database - const modelStats = getModelStats(model); - const baseMaxTokens = modelStats?.max_input_tokens; - // Check if 1M context is active and supported - const is1MActive = use1M && supports1MContext(model); - const maxTokens = is1MActive ? 1_000_000 : baseMaxTokens; - - // Total tokens includes cache creation (they're input tokens sent for caching) - const totalUsed = contextUsage - ? contextUsage.input.tokens + - contextUsage.cached.tokens + - contextUsage.cacheCreate.tokens + - contextUsage.output.tokens + - contextUsage.reasoning.tokens - : 0; - - // Calculate percentages based on max tokens (actual context window usage) - let inputPercentage: number; - let outputPercentage: number; - let cachedPercentage: number; - let cacheCreatePercentage: number; - let reasoningPercentage: number; - let showWarning = false; - let totalPercentage: number; - - if (maxTokens && contextUsage) { - // We know the model's max tokens - show actual context window usage - inputPercentage = (contextUsage.input.tokens / maxTokens) * 100; - outputPercentage = (contextUsage.output.tokens / maxTokens) * 100; - cachedPercentage = (contextUsage.cached.tokens / maxTokens) * 100; - cacheCreatePercentage = (contextUsage.cacheCreate.tokens / maxTokens) * 100; - reasoningPercentage = (contextUsage.reasoning.tokens / maxTokens) * 100; - totalPercentage = (totalUsed / maxTokens) * 100; - } else if (contextUsage) { - // Unknown model - scale to total tokens used - inputPercentage = totalUsed > 0 ? (contextUsage.input.tokens / totalUsed) * 100 : 0; - outputPercentage = - totalUsed > 0 ? (contextUsage.output.tokens / totalUsed) * 100 : 0; - cachedPercentage = - totalUsed > 0 ? (contextUsage.cached.tokens / totalUsed) * 100 : 0; - cacheCreatePercentage = - totalUsed > 0 ? (contextUsage.cacheCreate.tokens / totalUsed) * 100 : 0; - reasoningPercentage = - totalUsed > 0 ? (contextUsage.reasoning.tokens / totalUsed) * 100 : 0; - totalPercentage = 100; - showWarning = true; - } else { - inputPercentage = 0; - outputPercentage = 0; - cachedPercentage = 0; - cacheCreatePercentage = 0; - reasoningPercentage = 0; - totalPercentage = 0; - } - - const totalDisplay = formatTokens(totalUsed); - const maxDisplay = maxTokens ? ` / ${formatTokens(maxTokens)}` : ""; + const contextUsageData = contextUsage + ? calculateTokenMeterData(contextUsage, model, use1M, false) + : { segments: [], totalTokens: 0, totalPercentage: 0 }; return ( - <> -
-
- - Context Usage - - - {totalDisplay} - {maxDisplay} - {` (${totalPercentage.toFixed(1)}%)`} - -
-
- {/* Bar container - relative for slider positioning, overflow-hidden for rounded corners */} -
- {/* Segments container - flex layout for stacked percentages */} -
- {cachedPercentage > 0 && ( -
- )} - {cacheCreatePercentage > 0 && ( -
- )} -
-
- {reasoningPercentage > 0 && ( -
- )} -
-
- {/* Threshold slider overlay - positioned relative to outer container */} - {maxTokens && ( - - )} -
-
- {showWarning && ( -
- Unknown model limits - showing relative usage only -
- )} - + ); })()}
diff --git a/src/browser/components/RightSidebar/TokenMeter.tsx b/src/browser/components/RightSidebar/TokenMeter.tsx index 7cd8cb7481..679193d93c 100644 --- a/src/browser/components/RightSidebar/TokenMeter.tsx +++ b/src/browser/components/RightSidebar/TokenMeter.tsx @@ -6,6 +6,7 @@ interface TokenMeterProps { segments: TokenSegment[]; orientation: "horizontal" | "vertical"; className?: string; + trackClassName?: string; style?: React.CSSProperties; } @@ -13,13 +14,15 @@ const TokenMeterComponent: React.FC = ({ segments, orientation, className, + trackClassName, style, ...rest }) => { return (
Date: Tue, 16 Dec 2025 11:03:00 +0000 Subject: [PATCH 2/4] feat: remove collapsed right sidebar (context indicator now in ChatInput) - Remove 20px collapsed sidebar view since context usage is now shown in ChatInput - Remove chatAreaRef prop and hysteresis collapse/expand logic - Remove RIGHT_SIDEBAR_COLLAPSED_KEY storage constant - Simplify SidebarContainer to only handle width via customWidth/wide props - Vertical meter still shows on Review tab for continuity --- src/browser/components/AIView.tsx | 7 +- src/browser/components/RightSidebar.tsx | 101 +++++------------------- src/common/constants/storage.ts | 6 -- 3 files changed, 20 insertions(+), 94 deletions(-) diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index 69796a6ebe..b8eeda9faa 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -96,7 +96,6 @@ const AIViewInner: React.FC = ({ status, }) => { const { api } = useAPI(); - const chatAreaRef = useRef(null); // Track which right sidebar tab is selected (listener: true to sync with RightSidebar changes) const [selectedRightTab] = usePersistedState(RIGHT_SIDEBAR_TAB_KEY, "costs", { @@ -558,10 +557,7 @@ const AIViewInner: React.FC = ({ )} style={{ containerType: "inline-size" }} > -
+
= ({ key={workspaceId} workspaceId={workspaceId} workspacePath={namedWorkspacePath} - chatAreaRef={chatAreaRef} width={sidebarWidth} onStartResize={startResize} isResizing={isResizing} diff --git a/src/browser/components/RightSidebar.tsx b/src/browser/components/RightSidebar.tsx index 1d1535a284..5e3f9ed091 100644 --- a/src/browser/components/RightSidebar.tsx +++ b/src/browser/components/RightSidebar.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { RIGHT_SIDEBAR_TAB_KEY, RIGHT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage"; +import { RIGHT_SIDEBAR_TAB_KEY } from "@/common/constants/storage"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { useWorkspaceUsage } from "@/browser/stores/WorkspaceStore"; import { useProviderOptions } from "@/browser/hooks/useProviderOptions"; -import { useResizeObserver } from "@/browser/hooks/useResizeObserver"; + import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings"; import { CostsTab } from "./RightSidebar/CostsTab"; import { VerticalTokenMeter } from "./RightSidebar/VerticalTokenMeter"; @@ -22,7 +22,6 @@ export interface ReviewStats { } interface SidebarContainerProps { - collapsed: boolean; wide?: boolean; /** Custom width from drag-resize (persisted per-tab by AIView) */ customWidth?: number; @@ -37,13 +36,11 @@ interface SidebarContainerProps { * SidebarContainer - Main sidebar wrapper with dynamic width * * Width priority (first match wins): - * 1. collapsed (20px) - Shows vertical token meter only - * 2. customWidth - From drag-resize (persisted per-tab) - * 3. wide - Auto-calculated max width for Review tab (when not drag-resizing) - * 4. default (300px) - Costs tab when no customWidth saved + * 1. customWidth - From drag-resize (persisted per-tab) + * 2. wide - Auto-calculated max width for Review tab (when not drag-resizing) + * 3. default (300px) - Costs tab when no customWidth saved */ const SidebarContainer: React.FC = ({ - collapsed, wide, customWidth, isResizing, @@ -51,23 +48,19 @@ const SidebarContainer: React.FC = ({ role, "aria-label": ariaLabel, }) => { - const width = collapsed - ? "20px" - : customWidth - ? `${customWidth}px` - : wide - ? "min(1200px, calc(100vw - 400px))" - : "300px"; + const width = customWidth + ? `${customWidth}px` + : wide + ? "min(1200px, calc(100vw - 400px))" + : "300px"; return (
; /** Custom width in pixels (persisted per-tab, provided by AIView) */ width?: number; /** Drag start handler for resize */ @@ -101,7 +93,6 @@ interface RightSidebarProps { const RightSidebarComponent: React.FC = ({ workspaceId, workspacePath, - chatAreaRef, width, onStartResize, isResizing = false, @@ -137,7 +128,6 @@ const RightSidebarComponent: React.FC = ({ const usage = useWorkspaceUsage(workspaceId); const { options } = useProviderOptions(); const use1M = options.anthropic?.use1MContext ?? false; - const chatAreaSize = useResizeObserver(chatAreaRef); const baseId = `right-sidebar-${workspaceId}`; const costsTabId = `${baseId}-tab-costs`; @@ -180,57 +170,7 @@ const RightSidebarComponent: React.FC = ({ : { segments: [], totalTokens: 0, totalPercentage: 0 }; }, [lastUsage, model, use1M]); - // Calculate if we should show collapsed view with hysteresis - // Strategy: Observe ChatArea width directly (independent of sidebar width) - // - ChatArea has min-width: 750px and flex: 1 - // - Use hysteresis to prevent oscillation: - // * Collapse when chatAreaWidth <= 800px (tight space) - // * Expand when chatAreaWidth >= 1100px (lots of space) - // * Between 800-1100: maintain current state (dead zone) - const COLLAPSE_THRESHOLD = 800; // Collapse below this - const EXPAND_THRESHOLD = 1100; // Expand above this - const chatAreaWidth = chatAreaSize?.width ?? 1000; // Default to large to avoid flash - - // Persist collapsed state globally (not per-workspace) since chat area width is shared - // This prevents animation flash when switching workspaces - sidebar maintains its state - const [showCollapsed, setShowCollapsed] = usePersistedState( - RIGHT_SIDEBAR_COLLAPSED_KEY, - false - ); - - React.useEffect(() => { - // Never collapse when Review tab is active - code review needs space - if (selectedTab === "review") { - if (showCollapsed) { - setShowCollapsed(false); - } - 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, width]); - - // Single render point for VerticalTokenMeter - // Shows when: (1) collapsed, OR (2) Review tab is active - const showMeter = showCollapsed || selectedTab === "review"; + // Vertical meter only shows on Review tab (context usage indicator is now in ChatInput) const autoCompactionProps = React.useMemo( () => ({ threshold: autoCompactThreshold, @@ -238,21 +178,20 @@ const RightSidebarComponent: React.FC = ({ }), [autoCompactThreshold, setAutoCompactThreshold] ); - const verticalMeter = showMeter ? ( - - ) : null; + const verticalMeter = + selectedTab === "review" ? ( + + ) : null; return ( - {/* Full view when not collapsed */} -
+
{/* Resize handle (left edge) */} {onStartResize && (
= ({
- {/* Render meter in collapsed view when sidebar is collapsed */} -
{verticalMeter}
); }; // Memoize to prevent re-renders when parent (AIView) re-renders during streaming -// Only re-renders when workspaceId or chatAreaRef changes, or internal state updates +// Only re-renders when workspaceId changes, or internal state updates export const RightSidebar = React.memo(RightSidebarComponent); diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index 222a7bb841..cd9f4468ce 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -233,12 +233,6 @@ export function getStatusStateKey(workspaceId: string): string { */ export const RIGHT_SIDEBAR_TAB_KEY = "right-sidebar-tab"; -/** - * Right sidebar collapsed state (global) - * Format: "right-sidebar:collapsed" - */ -export const RIGHT_SIDEBAR_COLLAPSED_KEY = "right-sidebar:collapsed"; - /** * Right sidebar width for Costs tab (global) * Format: "right-sidebar:width:costs" From e10d042923c6b75cc2b1e4aa03cb23e3ca5d2a20 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:15:16 +0000 Subject: [PATCH 3/4] fix: restore auto-hide for right sidebar on small screens Instead of showing a 20px collapsed sidebar, now fully hides the sidebar when chat area width falls below threshold. Context usage is shown in ChatInput so no information is lost. - Restore chatAreaRef prop and useResizeObserver for width detection - Restore hysteresis collapse logic (800px collapse, 1100px expand) - Return null when hidden instead of showing collapsed meter view - Restore RIGHT_SIDEBAR_COLLAPSED_KEY storage constant --- src/browser/components/AIView.tsx | 7 +++- src/browser/components/RightSidebar.tsx | 54 +++++++++++++++++++++++-- src/common/constants/storage.ts | 6 +++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index b8eeda9faa..69796a6ebe 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -96,6 +96,7 @@ const AIViewInner: React.FC = ({ status, }) => { const { api } = useAPI(); + const chatAreaRef = useRef(null); // Track which right sidebar tab is selected (listener: true to sync with RightSidebar changes) const [selectedRightTab] = usePersistedState(RIGHT_SIDEBAR_TAB_KEY, "costs", { @@ -557,7 +558,10 @@ const AIViewInner: React.FC = ({ )} style={{ containerType: "inline-size" }} > -
+
= ({ key={workspaceId} workspaceId={workspaceId} workspacePath={namedWorkspacePath} + chatAreaRef={chatAreaRef} width={sidebarWidth} onStartResize={startResize} isResizing={isResizing} diff --git a/src/browser/components/RightSidebar.tsx b/src/browser/components/RightSidebar.tsx index 5e3f9ed091..01c90502d1 100644 --- a/src/browser/components/RightSidebar.tsx +++ b/src/browser/components/RightSidebar.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { RIGHT_SIDEBAR_TAB_KEY } from "@/common/constants/storage"; +import { RIGHT_SIDEBAR_TAB_KEY, RIGHT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage"; import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { useWorkspaceUsage } from "@/browser/stores/WorkspaceStore"; import { useProviderOptions } from "@/browser/hooks/useProviderOptions"; - +import { useResizeObserver } from "@/browser/hooks/useResizeObserver"; import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings"; import { CostsTab } from "./RightSidebar/CostsTab"; import { VerticalTokenMeter } from "./RightSidebar/VerticalTokenMeter"; @@ -78,6 +78,7 @@ export type { TabType }; interface RightSidebarProps { workspaceId: string; workspacePath: string; + chatAreaRef: React.RefObject; /** Custom width in pixels (persisted per-tab, provided by AIView) */ width?: number; /** Drag start handler for resize */ @@ -93,6 +94,7 @@ interface RightSidebarProps { const RightSidebarComponent: React.FC = ({ workspaceId, workspacePath, + chatAreaRef, width, onStartResize, isResizing = false, @@ -128,6 +130,7 @@ const RightSidebarComponent: React.FC = ({ const usage = useWorkspaceUsage(workspaceId); const { options } = useProviderOptions(); const use1M = options.anthropic?.use1MContext ?? false; + const chatAreaSize = useResizeObserver(chatAreaRef); const baseId = `right-sidebar-${workspaceId}`; const costsTabId = `${baseId}-tab-costs`; @@ -170,6 +173,46 @@ const RightSidebarComponent: React.FC = ({ : { segments: [], totalTokens: 0, totalPercentage: 0 }; }, [lastUsage, model, use1M]); + // Auto-hide sidebar on small screens using hysteresis to prevent oscillation + // - Observe ChatArea width directly (independent of sidebar width) + // - ChatArea has min-width and flex: 1 + // - Collapse when chatAreaWidth <= 800px (tight space) + // - Expand when chatAreaWidth >= 1100px (lots of space) + // - Between 800-1100: maintain current state (dead zone) + const COLLAPSE_THRESHOLD = 800; + const EXPAND_THRESHOLD = 1100; + const chatAreaWidth = chatAreaSize?.width ?? 1000; // Default to large to avoid flash + + // Persist collapsed state globally (not per-workspace) since chat area width is shared + const [isHidden, setIsHidden] = usePersistedState(RIGHT_SIDEBAR_COLLAPSED_KEY, false); + + React.useEffect(() => { + // Never hide when Review tab is active - code review needs space + if (selectedTab === "review") { + if (isHidden) { + setIsHidden(false); + } + return; + } + + // If sidebar is custom-resized wider than default, don't auto-hide + // (would cause oscillation between hidden and wide states) + if (width !== undefined && width > 300) { + if (isHidden) { + setIsHidden(false); + } + return; + } + + // Normal hysteresis for Costs tab + if (chatAreaWidth <= COLLAPSE_THRESHOLD) { + setIsHidden(true); + } else if (chatAreaWidth >= EXPAND_THRESHOLD) { + setIsHidden(false); + } + // Between thresholds: maintain current state (no change) + }, [chatAreaWidth, selectedTab, isHidden, setIsHidden, width]); + // Vertical meter only shows on Review tab (context usage indicator is now in ChatInput) const autoCompactionProps = React.useMemo( () => ({ @@ -183,6 +226,11 @@ const RightSidebarComponent: React.FC = ({ ) : null; + // Fully hide sidebar on small screens (context usage now shown in ChatInput) + if (isHidden) { + return null; + } + return ( = ({ }; // Memoize to prevent re-renders when parent (AIView) re-renders during streaming -// Only re-renders when workspaceId changes, or internal state updates +// Only re-renders when workspaceId or chatAreaRef changes, or internal state updates export const RightSidebar = React.memo(RightSidebarComponent); diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index cd9f4468ce..8a58ee698b 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -233,6 +233,12 @@ export function getStatusStateKey(workspaceId: string): string { */ export const RIGHT_SIDEBAR_TAB_KEY = "right-sidebar-tab"; +/** + * Right sidebar hidden state (global, auto-collapse on small screens) + * Format: "right-sidebar:hidden" + */ +export const RIGHT_SIDEBAR_COLLAPSED_KEY = "right-sidebar:hidden"; + /** * Right sidebar width for Costs tab (global) * Format: "right-sidebar:width:costs" From 647e48d97465bc6521383e2f0d7bbe5b47d634be Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:13:13 +0000 Subject: [PATCH 4/4] fix: tailwind class order --- src/browser/components/ContextUsageIndicatorButton.tsx | 2 +- src/browser/components/RightSidebar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/components/ContextUsageIndicatorButton.tsx b/src/browser/components/ContextUsageIndicatorButton.tsx index 05650b7ca2..828d4e1943 100644 --- a/src/browser/components/ContextUsageIndicatorButton.tsx +++ b/src/browser/components/ContextUsageIndicatorButton.tsx @@ -32,7 +32,7 @@ export const ContextUsageIndicatorButton: React.FC