Skip to content
Open
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
11 changes: 7 additions & 4 deletions src/browser/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
const { workspaceMetadata } = useWorkspaceContext();
const chatAreaRef = useRef<HTMLDivElement>(null);

// Track which right sidebar tab is selected (listener: true to sync with RightSidebar changes)
const [selectedRightTab] = usePersistedState<TabType>(RIGHT_SIDEBAR_TAB_KEY, "costs", {
listener: true,
});
// AIView owns tab state to ensure tab + width update atomically (prevents jank during tab switch)
const [selectedRightTab, setSelectedRightTab] = usePersistedState<TabType>(
RIGHT_SIDEBAR_TAB_KEY,
"costs"
);

// Resizable RightSidebar width - separate hooks per tab for independent persistence
const costsSidebar = useResizableSidebar({
Expand Down Expand Up @@ -794,6 +795,8 @@ const AIViewInner: React.FC<AIViewProps> = ({
key={workspaceId}
workspaceId={workspaceId}
workspacePath={namedWorkspacePath}
selectedTab={selectedRightTab}
onTabChange={setSelectedRightTab}
width={sidebarWidth}
onStartResize={startResize}
isResizing={isResizing}
Expand Down
29 changes: 16 additions & 13 deletions src/browser/components/RightSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { RIGHT_SIDEBAR_TAB_KEY, RIGHT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage";
import { RIGHT_SIDEBAR_COLLAPSED_KEY } from "@/common/constants/storage";
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import { useWorkspaceUsage, useWorkspaceStatsSnapshot } from "@/browser/stores/WorkspaceStore";
import { useFeatureFlags } from "@/browser/contexts/FeatureFlagsContext";
Expand Down Expand Up @@ -91,6 +91,10 @@ export type { TabType };
interface RightSidebarProps {
workspaceId: string;
workspacePath: string;
/** Currently selected tab (owned by AIView to sync with width) */
selectedTab: TabType;
/** Tab change handler */
onTabChange: (tab: TabType) => void;
/** Custom width in pixels (persisted per-tab, provided by AIView) */
width?: number;
/** Drag start handler for resize */
Expand All @@ -106,15 +110,14 @@ interface RightSidebarProps {
const RightSidebarComponent: React.FC<RightSidebarProps> = ({
workspaceId,
workspacePath,
selectedTab,
onTabChange,
width,
onStartResize,
isResizing = false,
onReviewNote,
isCreating = false,
}) => {
// Global tab preference (not per-workspace)
const [selectedTab, setSelectedTab] = usePersistedState<TabType>(RIGHT_SIDEBAR_TAB_KEY, "costs");

// Manual collapse state (persisted globally)
const [collapsed, setCollapsed] = usePersistedState<boolean>(RIGHT_SIDEBAR_COLLAPSED_KEY, false);

Expand All @@ -123,9 +126,9 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({

React.useEffect(() => {
if (!statsTabEnabled && selectedTab === "stats") {
setSelectedTab("costs");
onTabChange("costs");
}
}, [statsTabEnabled, selectedTab, setSelectedTab]);
}, [statsTabEnabled, selectedTab, onTabChange]);

// Trigger for focusing Review panel (preserves hunk selection)
const [focusTrigger, setFocusTrigger] = React.useState(0);
Expand All @@ -138,23 +141,23 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
const handleKeyDown = (e: KeyboardEvent) => {
if (matchesKeybind(e, KEYBINDS.COSTS_TAB)) {
e.preventDefault();
setSelectedTab("costs");
onTabChange("costs");
setCollapsed(false);
} else if (matchesKeybind(e, KEYBINDS.REVIEW_TAB)) {
e.preventDefault();
setSelectedTab("review");
onTabChange("review");
setCollapsed(false);
setFocusTrigger((prev) => prev + 1);
} else if (statsTabEnabled && matchesKeybind(e, KEYBINDS.STATS_TAB)) {
e.preventDefault();
setSelectedTab("stats");
onTabChange("stats");
setCollapsed(false);
}
};

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

const usage = useWorkspaceUsage(workspaceId);

Expand Down Expand Up @@ -232,7 +235,7 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
? "bg-hover text-foreground"
: "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground"
)}
onClick={() => setSelectedTab("costs")}
onClick={() => onTabChange("costs")}
id={costsTabId}
role="tab"
type="button"
Expand Down Expand Up @@ -260,7 +263,7 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
? "bg-hover text-foreground"
: "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground"
)}
onClick={() => setSelectedTab("review")}
onClick={() => onTabChange("review")}
id={reviewTabId}
role="tab"
type="button"
Expand Down Expand Up @@ -296,7 +299,7 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
? "bg-hover text-foreground"
: "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground"
)}
onClick={() => setSelectedTab("stats")}
onClick={() => onTabChange("stats")}
id={statsTabId}
role="tab"
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { applyFrontendFilters } from "@/browser/utils/review/filterHunks";
import { findNextHunkId, findNextHunkIdAfterFileRemoval } from "@/browser/utils/review/navigation";
import { cn } from "@/common/lib/utils";
import { useAPI, type APIClient } from "@/browser/contexts/API";
import { Loader2 } from "lucide-react";
import { workspaceStore } from "@/browser/stores/WorkspaceStore";
import { invalidateGitStatus } from "@/browser/stores/GitStatusStore";

Expand Down Expand Up @@ -1028,8 +1029,8 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
{diffState.message}
</div>
) : diffState.status === "loading" ? (
<div className="text-muted flex h-full items-center justify-center text-sm">
Loading diff...
<div className="flex h-full items-center justify-center">
<Loader2 className="text-muted h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
Expand Down