diff --git a/frontend/src/components/SessionView.tsx b/frontend/src/components/SessionView.tsx
index bc7cd07..69f5b82 100644
--- a/frontend/src/components/SessionView.tsx
+++ b/frontend/src/components/SessionView.tsx
@@ -973,7 +973,7 @@ export const SessionView = memo(() => {
.filter(p => !defaultTerminalPanel || p.id !== defaultTerminalPanel.id)
.map(panel => {
const isActive = panel.id === currentActivePanel.id;
- const shouldKeepAlive = ['terminal', 'browser'].includes(panel.type);
+ const shouldKeepAlive = ['terminal'].includes(panel.type);
if (!isActive && !shouldKeepAlive) return null;
return (
{
.filter(p => !defaultTerminalPanel || p.id !== defaultTerminalPanel.id)
.map(panel => {
const isActive = panel.id === currentActivePanel.id;
- const shouldKeepAlive = ['terminal', 'browser'].includes(panel.type);
+ const shouldKeepAlive = ['terminal'].includes(panel.type);
if (!isActive && !shouldKeepAlive) return null;
return (
= memo(({
const resourceChipRef = useRef
(null);
const resourcePopoverRef = useRef(null);
const [expandedSections, setExpandedSections] = useState>(new Set(['pane-app']));
- const { snapshot, startActive, stopActive, refresh } = useResourceMonitor();
+ const { snapshot, isLoading: resourceLoading, startActive, stopActive, refresh } = useResourceMonitor();
const [popoverStyle, setPopoverStyle] = useState({});
const getPanelActivityStatus = usePanelStore(s => s.getPanelActivityStatus);
@@ -150,12 +150,16 @@ export const PanelTabBar: React.FC = memo(({
// Resource monitor handlers
const toggleResourcePopover = useCallback(() => {
- setShowResourcePopover(prev => {
- if (!prev) startActive();
- else stopActive();
- return !prev;
- });
- }, [startActive, stopActive]);
+ if (showResourcePopover) {
+ setShowResourcePopover(false);
+ stopActive();
+ return;
+ }
+
+ setShowResourcePopover(true);
+ void refresh();
+ startActive();
+ }, [showResourcePopover, refresh, startActive, stopActive]);
// Popover positioning
useEffect(() => {
@@ -230,7 +234,7 @@ export const PanelTabBar: React.FC = memo(({
});
}, []);
- const handleRefresh = useCallback(() => { refresh(); }, [refresh]);
+ const handleRefresh = useCallback(() => { void refresh(); }, [refresh]);
const electronTotalCpu = useMemo(() =>
snapshot?.electronProcesses.reduce((sum, p) => sum + p.cpuPercent, 0) ?? 0
@@ -910,7 +914,7 @@ export const PanelTabBar: React.FC = memo(({
{/* Resource monitor popover */}
- {showResourcePopover && snapshot && createPortal(
+ {showResourcePopover && createPortal(
= memo(({
- {/* Summary */}
-
-
- CPU {snapshot.cpuReady ? `${snapshot.totalCpuPercent.toFixed(1)}%` : '—'}
-
-
- Memory {formatMemory(snapshot.totalMemoryMB)}
-
-
+ {!snapshot ? (
+
+ {resourceLoading ? 'Loading resource usage...' : 'No resource snapshot yet.'}
+
+ ) : (
+ <>
+ {/* Summary */}
+
+
+ CPU {snapshot.cpuReady ? `${snapshot.totalCpuPercent.toFixed(1)}%` : '—'}
+
+
+ Memory {formatMemory(snapshot.totalMemoryMB)}
+
+
- {/* Scrollable content */}
-
- {/* Pane App section */}
-
-
- {expandedSections.has('pane-app') && snapshot.electronProcesses.map(p => (
-
-
{p.label}
-
- {snapshot.cpuReady ? `${p.cpuPercent.toFixed(1)}%` : '—'}
- {formatMemory(p.memoryMB)}
-
+ {/* Scrollable content */}
+
+ {/* Pane App section */}
+
+
+ {expandedSections.has('pane-app') && snapshot.electronProcesses.map(p => (
+
+
{p.label}
+
+ {snapshot.cpuReady ? `${p.cpuPercent.toFixed(1)}%` : '—'}
+ {formatMemory(p.memoryMB)}
+
+
+ ))}
- ))}
-
- {/* Per-session sections */}
- {snapshot.sessions.map(sess => (
-
-
+ >
+ )}
,
document.body
)}
diff --git a/frontend/src/components/panels/diff/CombinedDiffView.tsx b/frontend/src/components/panels/diff/CombinedDiffView.tsx
index d992035..ac1b9e0 100644
--- a/frontend/src/components/panels/diff/CombinedDiffView.tsx
+++ b/frontend/src/components/panels/diff/CombinedDiffView.tsx
@@ -73,17 +73,29 @@ const CombinedDiffView = memo(forwardRef
(initialSelected);
const [lastSessionId, setLastSessionId] = useState(sessionId);
const [combinedDiff, setCombinedDiff] = useState(null);
- const [loading, setLoading] = useState(false);
+ const [executionsLoading, setExecutionsLoading] = useState(false);
+ const [diffLoading, setDiffLoading] = useState(false);
+ const [commitDiffLoading, setCommitDiffLoading] = useState(false);
const [error, setError] = useState(null);
const [viewingCommitHash, setViewingCommitHash] = useState(null);
const [showCommitDialog, setShowCommitDialog] = useState(false);
const [mainBranch, setMainBranch] = useState('main');
const [historySource, setHistorySource] = useState<'remote' | 'local' | 'branch'>(isMainRepo ? 'remote' : 'branch');
- const [forceRefresh, setForceRefresh] = useState(0);
+ const [executionRefreshNonce, setExecutionRefreshNonce] = useState(0);
// Diff cache: keyed by sessionId + sorted selection
const diffCacheRef = useRef
@@ -621,18 +662,13 @@ const CombinedDiffView = memo(forwardRef
Please wait while the operation completes...
- ) : loading && combinedDiff === null ? (
+ ) : showDiffSkeleton ? (
diff --git a/frontend/src/components/panels/diff/DiffPanel.tsx b/frontend/src/components/panels/diff/DiffPanel.tsx
index 1e7041b..6584426 100644
--- a/frontend/src/components/panels/diff/DiffPanel.tsx
+++ b/frontend/src/components/panels/diff/DiffPanel.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import CombinedDiffView from './CombinedDiffView';
import type { CombinedDiffViewHandle } from './CombinedDiffView';
import type { ToolPanel, DiffPanelState } from '../../../../../shared/types/panels';
+import type { GitStatus } from '../../../types/session';
import { AlertCircle } from 'lucide-react';
interface DiffPanelProps {
@@ -11,6 +12,23 @@ interface DiffPanelProps {
isMainRepo?: boolean;
}
+function buildGitStatusFingerprint(gitStatus?: GitStatus): string {
+ return [
+ gitStatus?.state ?? 'unknown',
+ gitStatus?.ahead ?? 0,
+ gitStatus?.behind ?? 0,
+ gitStatus?.hasUncommittedChanges ? 1 : 0,
+ gitStatus?.hasUntrackedFiles ? 1 : 0,
+ gitStatus?.filesChanged ?? 0,
+ gitStatus?.additions ?? 0,
+ gitStatus?.deletions ?? 0,
+ gitStatus?.commitAdditions ?? 0,
+ gitStatus?.commitDeletions ?? 0,
+ gitStatus?.commitFilesChanged ?? 0,
+ gitStatus?.totalCommits ?? 0,
+ ].join(':');
+}
+
export const DiffPanel: React.FC = ({
panel,
isActive,
@@ -22,7 +40,8 @@ export const DiffPanel: React.FC = ({
const lastRefreshRef = useRef(Date.now());
const combinedDiffRef = useRef(null);
// Track diff-relevant git state to avoid spurious refreshes on no-op status events
- const lastGitFingerprintRef = useRef('');
+ const lastGitFingerprintRef = useRef(null);
+ const wasActiveRef = useRef(isActive);
// Listen for file change events from other panels
useEffect(() => {
@@ -55,12 +74,18 @@ export const DiffPanel: React.FC = ({
// Listen for git-status-updated events (detects new commits from Claude, etc.)
// Only mark stale when diff-relevant state actually changes, not on no-op refreshes
useEffect(() => {
+ lastGitFingerprintRef.current = null;
+
const handleGitStatusUpdated = (event: Event) => {
- const { sessionId: eventSessionId, gitStatus } = (event as CustomEvent).detail || {};
+ const { sessionId: eventSessionId, gitStatus } = (event as CustomEvent<{ sessionId: string; gitStatus?: GitStatus }>).detail || {};
if (eventSessionId !== sessionId) return;
// Fingerprint the diff-relevant fields — ignore no-op status refreshes
- const fingerprint = `${gitStatus?.state}-${gitStatus?.ahead}-${gitStatus?.behind}-${gitStatus?.uncommittedChanges}`;
+ const fingerprint = buildGitStatusFingerprint(gitStatus);
+ if (lastGitFingerprintRef.current === null) {
+ lastGitFingerprintRef.current = fingerprint;
+ return;
+ }
if (fingerprint === lastGitFingerprintRef.current) return;
lastGitFingerprintRef.current = fingerprint;
@@ -73,7 +98,10 @@ export const DiffPanel: React.FC = ({
// Auto-refresh when becoming active and stale
useEffect(() => {
- if (isActive && isStale) {
+ const becameActive = isActive && !wasActiveRef.current;
+ wasActiveRef.current = isActive;
+
+ if (becameActive && isStale) {
setIsStale(false);
combinedDiffRef.current?.refresh();
diff --git a/frontend/src/hooks/useResourceMonitor.ts b/frontend/src/hooks/useResourceMonitor.ts
index 28cb26c..47df003 100644
--- a/frontend/src/hooks/useResourceMonitor.ts
+++ b/frontend/src/hooks/useResourceMonitor.ts
@@ -9,7 +9,7 @@ interface IPCResponse {
export function useResourceMonitor() {
const [snapshot, setSnapshot] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
+ const [isLoading, setIsLoading] = useState(false);
// Listen for push updates from main process
useEffect(() => {
@@ -20,14 +20,6 @@ export function useResourceMonitor() {
};
window.addEventListener('resource-monitor:update', handler);
- // Also fetch initial snapshot
- window.electronAPI?.resourceMonitor?.getSnapshot?.().then((response: IPCResponse) => {
- if (response?.success && response.data) {
- setSnapshot(response.data);
- }
- setIsLoading(false);
- });
-
return () => {
window.removeEventListener('resource-monitor:update', handler);
// Stop active polling if component unmounts while popover is open
@@ -44,9 +36,14 @@ export function useResourceMonitor() {
}, []);
const refresh = useCallback(async () => {
- const response: IPCResponse | undefined = await window.electronAPI?.resourceMonitor?.getSnapshot?.();
- if (response?.success && response.data) {
- setSnapshot(response.data);
+ setIsLoading(true);
+ try {
+ const response: IPCResponse | undefined = await window.electronAPI?.resourceMonitor?.getSnapshot?.();
+ if (response?.success && response.data) {
+ setSnapshot(response.data);
+ }
+ } finally {
+ setIsLoading(false);
}
}, []);
diff --git a/main/src/index.ts b/main/src/index.ts
index 0bac00f..258da44 100644
--- a/main/src/index.ts
+++ b/main/src/index.ts
@@ -9,7 +9,7 @@ if (process.platform === 'linux') {
}
// Force integrated GPU for better battery life on dual-GPU systems
-app.commandLine.appendSwitch('force_discrete_gpu', '0');
+app.commandLine.appendSwitch('force_low_power_gpu');
// Set Windows AUMID to match electron-builder's appId so Windows resolves
// the installed Start Menu shortcut for notification icon and display name.
@@ -873,11 +873,9 @@ async function createWindow() {
mainWindow.on('restore', () => {
// Don't assume restore = focused. The OS will fire 'focus' if/when the user
- // actually focuses the window. Keep git/resource hooks as-is.
- if (gitStatusManager) {
- gitStatusManager.handleVisibilityChange(false); // false = visible/restored
- }
- resourceMonitorService.handleVisibilityChange(false);
+ // actually focuses the window; that is what restarts git/resource work.
+ const focused = mainWindow?.isFocused() ?? false;
+ mainWindow?.webContents.send('window:focus-changed', focused);
});
// Hand the renderer its per-window ptyHost data port once the preload
@@ -1081,7 +1079,6 @@ async function initializeServices() {
// Start resource monitoring
resourceMonitorService.initialize(app);
- resourceMonitorService.startIdlePolling();
// Restore spotlight state from previous session
try {
diff --git a/main/src/services/resourceMonitorService.ts b/main/src/services/resourceMonitorService.ts
index 89c097d..8642199 100644
--- a/main/src/services/resourceMonitorService.ts
+++ b/main/src/services/resourceMonitorService.ts
@@ -374,14 +374,13 @@ export class ResourceMonitorService extends EventEmitter {
this.pollInProgress = false;
}
};
- void poll();
this.activeTimer = setInterval(() => void poll(), 5_000);
}
stopActivePolling(): void {
if (this.isActivePolling) {
this.isActivePolling = false;
- this.startIdlePolling();
+ this.stopAllPolling();
}
}
@@ -391,12 +390,8 @@ export class ResourceMonitorService extends EventEmitter {
this.stopAllPolling();
this.previousCpuSamples.clear();
this.needsCpuWarmup = true;
- } else {
- if (this.isActivePolling) {
- this.startActivePolling();
- } else {
- this.startIdlePolling();
- }
+ } else if (this.isActivePolling) {
+ this.startActivePolling();
}
}