Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c5e76e2
πŸ€– feat: add timing stats tab in right sidebar
ThomasK33 Dec 15, 2025
0eb2af1
feat: persist timing stats after stream completes
ThomasK33 Dec 15, 2025
5823942
fix: include in-progress tool execution time in stats
ThomasK33 Dec 15, 2025
0156703
feat: add session-level aggregate timing stats
ThomasK33 Dec 15, 2025
216a9c5
feat: redesign StatsTab to match CostsTab layout
ThomasK33 Dec 15, 2025
24774e3
fix: update storybook tests for new StatsTab UI
ThomasK33 Dec 15, 2025
642ec3f
fix: remove TTFT from progress bar
ThomasK33 Dec 15, 2025
f104e4b
feat: live update session stats during active stream
ThomasK33 Dec 15, 2025
ea2f972
feat: persist session timing stats to localStorage
ThomasK33 Dec 15, 2025
20430d1
fix: update session avg TTFT live and spell out label
ThomasK33 Dec 15, 2025
0e45401
fix: use nullish coalescing and fix formatting
ThomasK33 Dec 15, 2025
9e7b1ec
feat: track per-model timing stats for future breakdown
ThomasK33 Dec 15, 2025
4fcc501
refactor: compute session totals on-the-fly from per-model data
ThomasK33 Dec 15, 2025
c12578b
refactor: flatten sessionTimingStats to Record<model, stats>
ThomasK33 Dec 15, 2025
5d513d6
πŸ€– feat: add model filtering to Stats tab
ThomasK33 Dec 16, 2025
3e7499b
πŸ€– refactor: use formatModelDisplayName utility for model names
ThomasK33 Dec 16, 2025
f077d70
πŸ€– feat: show Session/Last Request toggle + per-model breakdown
ThomasK33 Dec 16, 2025
382e695
πŸ€– feat: show duration in Stats tab title + per-model breakdown always
ThomasK33 Dec 16, 2025
4508cd5
πŸ€– fix: live update Stats tab title during active stream
ThomasK33 Dec 16, 2025
b4cbdef
πŸ€– feat: add tokens/sec metric to per-model breakdown
ThomasK33 Dec 16, 2025
02e154e
πŸ€– fix: include active stream in per-model breakdown
ThomasK33 Dec 16, 2025
08a0498
πŸ€– feat: add avg tokens/msg and tokens/think to per-model stats
ThomasK33 Dec 16, 2025
9feb7d9
πŸ€– feat: show total output tokens in session stats
ThomasK33 Dec 16, 2025
188e92c
πŸ€– refactor: add parity between Session and Last Request views
ThomasK33 Dec 16, 2025
809a530
πŸ€– fix: use streaming time for accurate tokens/sec calculation
ThomasK33 Dec 16, 2025
e96b77e
πŸ€– feat: show live tokens/sec during active streaming
ThomasK33 Dec 16, 2025
db9f6eb
πŸ€– feat: add mode breakdown and fix tokens/sec calculation
ThomasK33 Dec 16, 2025
d548f5a
πŸ€– fix: exclude tool execution time from tokens/sec calculation
ThomasK33 Dec 16, 2025
2700da2
πŸ€– fix: propagate backend startTime to fix negative TTFT on replay
ThomasK33 Dec 16, 2025
28efaf4
πŸ€– feat: add 'Clear Timing Stats' command to palette
ThomasK33 Dec 16, 2025
80b7d5a
πŸ€– fix: add sanity check for TPS calculation
ThomasK33 Dec 16, 2025
7af1b77
πŸ€– feat: add mode to stream-start events for plan/exec breakdown
ThomasK33 Dec 16, 2025
dfaab9d
πŸ€– fix: avoid mixed server/client clocks in timing stats
ThomasK33 Dec 16, 2025
3fe8207
πŸ€– fix: enable right sidebar resizing on stats tab
ThomasK33 Dec 16, 2025
77aaf37
πŸ€– fix: include live timing fields in sidebar state cache
ThomasK33 Dec 16, 2025
24568c9
πŸ€– fix: consolidate per-model entries when not splitting by mode
ThomasK33 Dec 16, 2025
27a8b07
πŸ€– fix: correct token totals + refactor stats computations
ThomasK33 Dec 16, 2025
073ec8f
πŸ€– fix: polish stats tab (error boundary + clear button)
ThomasK33 Dec 16, 2025
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
1 change: 1 addition & 0 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ function AppInner() {
onToggleTheme: toggleTheme,
onSetTheme: setThemePreference,
onOpenSettings: openSettings,
onClearTimingStats: (workspaceId: string) => workspaceStore.clearTimingStats(workspaceId),
api,
};

Expand Down
3 changes: 2 additions & 1 deletion src/browser/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ const AIViewInner: React.FC<AIViewProps> = ({

// Resizable RightSidebar width - separate hooks per tab for independent persistence
const costsSidebar = useResizableSidebar({
enabled: selectedRightTab === "costs",
// Costs + Stats share the same resizable width persistence
enabled: selectedRightTab === "costs" || selectedRightTab === "stats",
defaultWidth: 300,
minWidth: 300,
maxWidth: 1200,
Expand Down
71 changes: 69 additions & 2 deletions src/browser/components/RightSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React from "react";
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 { useWorkspaceUsage, useWorkspaceSidebarState } from "@/browser/stores/WorkspaceStore";
import { useProviderOptions } from "@/browser/hooks/useProviderOptions";
import { useResizeObserver } from "@/browser/hooks/useResizeObserver";
import { useLiveTimer } from "@/browser/hooks/useLiveTimer";
import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings";
import { ErrorBoundary } from "./ErrorBoundary";
import { CostsTab } from "./RightSidebar/CostsTab";
import { StatsTab } from "./RightSidebar/StatsTab";
import { VerticalTokenMeter } from "./RightSidebar/VerticalTokenMeter";
import { ReviewPanel } from "./RightSidebar/CodeReview/ReviewPanel";
import { calculateTokenMeterData } from "@/common/utils/tokens/tokenMeterUtils";
Expand All @@ -21,6 +24,15 @@ export interface ReviewStats {
read: number;
}

/** Format duration for tab display (compact format) */
function formatTabDuration(ms: number): string {
if (ms < 1000) return `${Math.round(ms)}ms`;
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
const mins = Math.floor(ms / 60000);
const secs = Math.round((ms % 60000) / 1000);
return secs > 0 ? `${mins}m${secs}s` : `${mins}m`;
}

interface SidebarContainerProps {
collapsed: boolean;
wide?: boolean;
Expand Down Expand Up @@ -78,7 +90,7 @@ const SidebarContainer: React.FC<SidebarContainerProps> = ({
);
};

type TabType = "costs" | "review";
type TabType = "costs" | "stats" | "review";

export type { TabType };

Expand Down Expand Up @@ -127,6 +139,9 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
e.preventDefault();
setSelectedTab("review");
setFocusTrigger((prev) => prev + 1);
} else if (matchesKeybind(e, KEYBINDS.STATS_TAB)) {
e.preventDefault();
setSelectedTab("stats");
}
};

Expand All @@ -141,8 +156,10 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({

const baseId = `right-sidebar-${workspaceId}`;
const costsTabId = `${baseId}-tab-costs`;
const statsTabId = `${baseId}-tab-stats`;
const reviewTabId = `${baseId}-tab-review`;
const costsPanelId = `${baseId}-panel-costs`;
const statsPanelId = `${baseId}-panel-stats`;
const reviewPanelId = `${baseId}-panel-review`;

// Use lastContextUsage for context window display (last step = actual context size)
Expand All @@ -169,6 +186,21 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
return total > 0 ? total : null;
}, [usage.sessionTotal, usage.liveCostUsage]);

// Get timing stats for tab display
const { timingStats, sessionStats } = useWorkspaceSidebarState(workspaceId);

// Live timer for active streams (updates tab title every second)
const now = useLiveTimer(Boolean(timingStats?.isActive));

const sessionDuration = React.useMemo(() => {
// Include active stream duration if present
const baseDuration = sessionStats?.totalDurationMs ?? 0;
if (timingStats?.isActive) {
return baseDuration + (now - timingStats.startTime);
}
return baseDuration > 0 ? baseDuration : null;
}, [sessionStats?.totalDurationMs, timingStats?.isActive, timingStats?.startTime, now]);

// Auto-compaction settings: threshold per-model
const { threshold: autoCompactThreshold, setThreshold: setAutoCompactThreshold } =
useAutoCompactionSettings(workspaceId, model);
Expand Down Expand Up @@ -338,6 +370,34 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
{formatKeybind(KEYBINDS.REVIEW_TAB)}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition-all duration-150 flex items-baseline gap-1.5",
selectedTab === "stats"
? "bg-hover text-foreground"
: "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground"
)}
onClick={() => setSelectedTab("stats")}
id={statsTabId}
role="tab"
type="button"
aria-selected={selectedTab === "stats"}
aria-controls={statsPanelId}
>
Stats
{sessionDuration !== null && (
<span className="text-muted text-[10px]">
{formatTabDuration(sessionDuration)}
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
{formatKeybind(KEYBINDS.STATS_TAB)}
</TooltipContent>
</Tooltip>
</div>
<div
className={cn("flex-1 overflow-y-auto", selectedTab === "review" ? "p-0" : "p-[15px]")}
Expand Down Expand Up @@ -365,6 +425,13 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
/>
</div>
)}
{selectedTab === "stats" && (
<div role="tabpanel" id={statsPanelId} aria-labelledby={statsTabId}>
<ErrorBoundary workspaceInfo="Stats tab">
<StatsTab workspaceId={workspaceId} now={now} />
</ErrorBoundary>
</div>
)}
</div>
</div>
</div>
Expand Down
Loading