Skip to content

Commit 67a0aac

Browse files
committed
fix: improve error handling, race conditions, and memory leaks in git/source control components
Phase 1 - MultiRepoContext.tsx: - Add concurrency guard (refreshInFlight Map) to prevent overlapping refreshRepository calls - Wrap apiCall operations with try/catch + gitLogger.error for stage/unstage/discard/checkout/branch/merge - Wire up executeGitOperation (renamed from _executeGitOperation) for push/pull/fetch/commit with retry logic - Remove unused void suppression for _executeGitOperation - Re-lookup repo index after async operations to avoid stale index bugs Phase 2 - Component error states (8 files): - StashPanel: Add [error, setError] + [operationLoading, setOperationLoading] signals, error banner UI - BlameView: Add [error, setError] signal, error banner UI - CommitGraph: Add [error, setError] signal, error banner UI - BranchComparison: Add [error, setError] signal, error banner UI - DiffView: Add [error, setError] signal, error banner UI - CortexGitPanel: Add [error, setError] signal, error banner UI, surface stash operation errors - CortexSourceControl: Add [error, setError] signal, error banner UI - CortexGitHistory: Add [error, setError] signal, error banner UI - GitPanel: Fix error timeout leak - store timer ID and clear in onCleanup Phase 3 - Memory leak fixes: - CortexChangesPanel: Add onCleanup for drag event listeners (mousemove/mouseup) that could leak if component unmounts during drag
1 parent 16a0faa commit 67a0aac

File tree

11 files changed

+317
-97
lines changed

11 files changed

+317
-97
lines changed

src/components/cortex/CortexChangesPanel.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Wired to git_status and git_diff backend commands
55
*/
66

7-
import { Component, For, Show, createSignal, onMount, JSX } from "solid-js";
7+
import { Component, For, Show, createSignal, onMount, onCleanup, JSX } from "solid-js";
88
import { invoke } from "@tauri-apps/api/core";
99
import { CortexIcon } from "./primitives/CortexIcon";
1010
import { VibeTabBar, type VibeTab } from "./vibe/VibeTabBar";
@@ -97,6 +97,8 @@ export const CortexChangesPanel: Component<CortexChangesPanelProps> = (props) =>
9797
}
9898
};
9999

100+
let dragCleanup: (() => void) | null = null;
101+
100102
const handleDivider = (e: MouseEvent) => {
101103
e.preventDefault();
102104
const startY = e.clientY;
@@ -106,11 +108,18 @@ export const CortexChangesPanel: Component<CortexChangesPanelProps> = (props) =>
106108
const onUp = () => {
107109
document.removeEventListener("mousemove", onMove);
108110
document.removeEventListener("mouseup", onUp);
111+
dragCleanup = null;
109112
};
110113
document.addEventListener("mousemove", onMove);
111114
document.addEventListener("mouseup", onUp);
115+
dragCleanup = () => {
116+
document.removeEventListener("mousemove", onMove);
117+
document.removeEventListener("mouseup", onUp);
118+
};
112119
};
113120

121+
onCleanup(() => { dragCleanup?.(); });
122+
114123
onMount(async () => {
115124
if (props.changes.length === 0 && props.projectPath) {
116125
try {

src/components/cortex/CortexGitHistory.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ const emptyStyle: JSX.CSSProperties = {
165165
export const CortexGitHistory: Component<CortexGitHistoryProps> = (props) => {
166166
const [commits, setCommits] = createSignal<GitCommit[]>([]);
167167
const [loading, setLoading] = createSignal(true);
168+
const [error, setError] = createSignal<string | null>(null);
168169
const [searchQuery, setSearchQuery] = createSignal("");
169170
const [expandedHash, setExpandedHash] = createSignal<string | null>(null);
170171

@@ -186,11 +187,13 @@ export const CortexGitHistory: Component<CortexGitHistoryProps> = (props) => {
186187
return;
187188
}
188189
setLoading(true);
190+
setError(null);
189191
try {
190192
const result = await gitLog(projectPath, 100);
191193
setCommits(result);
192194
} catch (e) {
193195
logger.warn("Failed to fetch git history", e);
196+
setError(`Failed to load history: ${e}`);
194197
} finally {
195198
setLoading(false);
196199
}
@@ -229,6 +232,13 @@ export const CortexGitHistory: Component<CortexGitHistoryProps> = (props) => {
229232
</div>
230233

231234
<div style={listContainerStyle}>
235+
<Show when={error()}>
236+
<div style={{ display: "flex", "align-items": "center", gap: "8px", padding: "8px 16px", background: "rgba(239,68,68,0.1)", color: "#ef4444", "font-size": "12px" }}>
237+
<span style={{ flex: 1, overflow: "hidden", "text-overflow": "ellipsis", "white-space": "nowrap" }}>{error()}</span>
238+
<button onClick={() => setError(null)} style={{ background: "transparent", border: "none", color: "#ef4444", cursor: "pointer", padding: "2px", "font-size": "12px" }}></button>
239+
</div>
240+
</Show>
241+
232242
<Show when={loading()}>
233243
<div style={loadingStyle}>
234244
<span>Loading history…</span>

src/components/cortex/CortexGitPanel.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const CortexGitPanel: Component = () => {
6969
const [expandStash, setExpandStash] = createSignal(false);
7070
const [stashes, setStashes] = createSignal<StashEntry[]>([]);
7171
const [stashLoading, setStashLoading] = createSignal(false);
72+
const [error, setError] = createSignal<string | null>(null);
7273
let dotsRef: HTMLDivElement | undefined;
7374

7475
const repo = () => multiRepo?.activeRepository() ?? null;
@@ -109,19 +110,22 @@ export const CortexGitPanel: Component = () => {
109110
const handleStashCreate = async () => {
110111
const r = repo();
111112
if (!r) return;
112-
try { await gitStashCreate(r.path, "", true); await fetchStashes(); refresh(); } catch { /* ignore */ }
113+
setError(null);
114+
try { await gitStashCreate(r.path, "", true); await fetchStashes(); refresh(); } catch (err) { setError(`Stash create failed: ${err}`); }
113115
};
114116

115117
const handleStashPop = async (index: number) => {
116118
const r = repo();
117119
if (!r) return;
118-
try { await gitStashPop(r.path, index); await fetchStashes(); refresh(); } catch { /* ignore */ }
120+
setError(null);
121+
try { await gitStashPop(r.path, index); await fetchStashes(); refresh(); } catch (err) { setError(`Stash pop failed: ${err}`); }
119122
};
120123

121124
const handleStashDrop = async (index: number) => {
122125
const r = repo();
123126
if (!r) return;
124-
try { await gitStashDrop(r.path, index); await fetchStashes(); } catch { /* ignore */ }
127+
setError(null);
128+
try { await gitStashDrop(r.path, index); await fetchStashes(); } catch (err) { setError(`Stash drop failed: ${err}`); }
125129
};
126130

127131
const dotsAction = (action: string) => {
@@ -201,6 +205,14 @@ export const CortexGitPanel: Component = () => {
201205
</label>
202206
</div>
203207

208+
<Show when={error()}>
209+
<div style={{ display: "flex", "align-items": "center", gap: "8px", padding: "8px 12px", background: "rgba(239,68,68,0.1)", color: "#ef4444", "font-size": "13px" }}>
210+
<CortexIcon name="alert-circle" size={14} color="#ef4444" />
211+
<span style={{ flex: 1, overflow: "hidden", "text-overflow": "ellipsis", "white-space": "nowrap" }}>{error()}</span>
212+
<button onClick={() => setError(null)} style={{ background: "transparent", border: "none", color: "#ef4444", cursor: "pointer", padding: "2px" }}></button>
213+
</div>
214+
</Show>
215+
204216
<div style={{ flex: 1, overflow: "auto" }}>
205217
<SectionHeader title="Changes" count={unstaged().length} expanded={expandChanges()} onToggle={() => setExpandChanges((v) => !v)} actions={
206218
<>

src/components/cortex/CortexSourceControl.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export const CortexSourceControl: Component = () => {
8585
const [sections, setSections] = createSignal<Set<string>>(new Set(["staged", "changes"]));
8686
const [showCommits, setShowCommits] = createSignal(false);
8787
const [commits, setCommits] = createSignal<GitCommit[]>([]);
88+
const [error, setError] = createSignal<string | null>(null);
8889

8990
const repo = () => multiRepo?.activeRepository() ?? null;
9091
const staged = () => repo()?.stagedFiles ?? [];
@@ -97,7 +98,7 @@ export const CortexSourceControl: Component = () => {
9798

9899
const fetchCommits = async () => {
99100
const r = repo(); if (!r) return;
100-
try { setCommits(await gitLog(r.path, 5)); } catch { setCommits([]); }
101+
try { setCommits(await gitLog(r.path, 5)); } catch (err) { setCommits([]); setError(`Failed to load commits: ${err}`); }
101102
};
102103

103104
onMount(() => { if (repo()) fetchCommits(); });
@@ -129,6 +130,13 @@ export const CortexSourceControl: Component = () => {
129130
</div>
130131
</div>
131132
}>
133+
<Show when={error()}>
134+
<div style={{ display: "flex", "align-items": "center", gap: "8px", padding: "8px 16px", background: "rgba(239,68,68,0.1)", color: "#ef4444", "font-size": "12px" }}>
135+
<span style={{ flex: 1, overflow: "hidden", "text-overflow": "ellipsis", "white-space": "nowrap" }}>{error()}</span>
136+
<button onClick={() => setError(null)} style={{ background: "transparent", border: "none", color: "#ef4444", cursor: "pointer", padding: "2px", "font-size": "12px" }}></button>
137+
</div>
138+
</Show>
139+
132140
<div style={{ display: "flex", "align-items": "center", "justify-content": "space-between", padding: "12px 16px", "border-bottom": "1px solid var(--cortex-bg-hover)" }}>
133141
<div style={{ display: "flex", "align-items": "center", gap: "8px" }}>
134142
<span style={{ "font-weight": "500" }}>Source Control</span>

src/components/git/BlameView.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface BlameViewProps {
3333
export function BlameView(props: BlameViewProps) {
3434
const [blameData, setBlameData] = createSignal<BlameLine[]>([]);
3535
const [loading, setLoading] = createSignal(false);
36+
const [error, setError] = createSignal<string | null>(null);
3637
const [hoveredCommit, setHoveredCommit] = createSignal<string | null>(null);
3738
const [selectedCommit, setSelectedCommit] = createSignal<string | null>(null);
3839
const [copiedHash, setCopiedHash] = createSignal<string | null>(null);
@@ -42,6 +43,7 @@ export function BlameView(props: BlameViewProps) {
4243

4344
const fetchBlame = async (file: string) => {
4445
setLoading(true);
46+
setError(null);
4547
try {
4648
const projectPath = getProjectPath();
4749

@@ -81,6 +83,7 @@ export function BlameView(props: BlameViewProps) {
8183
}
8284
} catch (err) {
8385
console.error("Failed to fetch blame:", err);
86+
setError(`Failed to load blame data: ${err}`);
8487
} finally {
8588
setLoading(false);
8689
}
@@ -219,6 +222,19 @@ export function BlameView(props: BlameViewProps) {
219222
</div>
220223
}
221224
>
225+
<Show when={error()}>
226+
<div
227+
class="flex items-center gap-2 px-3 py-2 text-sm"
228+
style={{ background: "var(--status-error-bg, rgba(239,68,68,0.1))", color: "var(--status-error, #ef4444)" }}
229+
>
230+
<Icon name="circle-exclamation" class="w-4 h-4 shrink-0" />
231+
<span class="flex-1 truncate">{error()}</span>
232+
<button class="p-0.5 rounded hover:bg-white/10" onClick={() => setError(null)}>
233+
<Icon name="xmark" class="w-3.5 h-3.5" />
234+
</button>
235+
</div>
236+
</Show>
237+
222238
<Show when={loading()}>
223239
<div class="flex items-center justify-center h-full">
224240
<span style={{ color: "var(--text-weak)" }}>Loading blame...</span>

src/components/git/BranchComparison.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function BranchComparison(props: BranchComparisonProps) {
4242
const [baseBranch, setBaseBranch] = createSignal(props.baseBranch || "");
4343
const [compareBranch, setCompareBranch] = createSignal(props.compareBranch || "");
4444
const [loading, setLoading] = createSignal(false);
45+
const [error, setError] = createSignal<string | null>(null);
4546
const [stats, setStats] = createSignal<BranchCompareStats | null>(null);
4647
const [showBaseDropdown, setShowBaseDropdown] = createSignal(false);
4748
const [showCompareDropdown, setShowCompareDropdown] = createSignal(false);
@@ -64,6 +65,7 @@ export function BranchComparison(props: BranchComparisonProps) {
6465

6566
const fetchComparison = async (base: string, compare: string) => {
6667
setLoading(true);
68+
setError(null);
6769
try {
6870
const projectPath = getProjectPath();
6971
const data: GitCompareResult = await gitCompare(projectPath, base, compare);
@@ -91,6 +93,7 @@ export function BranchComparison(props: BranchComparisonProps) {
9193
});
9294
} catch (err) {
9395
console.error("Failed to fetch comparison:", err);
96+
setError(`Failed to compare branches: ${err}`);
9497
} finally {
9598
setLoading(false);
9699
}
@@ -274,6 +277,20 @@ export function BranchComparison(props: BranchComparisonProps) {
274277
</div>
275278
</div>
276279

280+
{/* Error Banner */}
281+
<Show when={error()}>
282+
<div
283+
class="flex items-center gap-2 px-3 py-2 text-sm"
284+
style={{ background: "var(--status-error-bg, rgba(239,68,68,0.1))", color: "var(--status-error, #ef4444)" }}
285+
>
286+
<Icon name="circle-exclamation" class="w-4 h-4 shrink-0" />
287+
<span class="flex-1 truncate">{error()}</span>
288+
<button class="p-0.5 rounded hover:bg-white/10" onClick={() => setError(null)}>
289+
<Icon name="xmark" class="w-3.5 h-3.5" />
290+
</button>
291+
</div>
292+
</Show>
293+
277294
{/* Stats summary */}
278295
<Show when={stats()}>
279296
<div

src/components/git/CommitGraph.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ function CommitRow(props: CommitRowProps) {
220220
export function CommitGraph(props: CommitGraphProps) {
221221
const [commits, setCommits] = createSignal<Commit[]>(props.commits || []);
222222
const [loading, setLoading] = createSignal(false);
223+
const [error, setError] = createSignal<string | null>(null);
223224
const [searchQuery, setSearchQuery] = createSignal("");
224225
const [selectedHash, setSelectedHash] = createSignal<string | null>(props.selectedCommit || null);
225226
const [contextMenuPos, setContextMenuPos] = createSignal<{ x: number; y: number; commit: Commit } | null>(null);
@@ -265,6 +266,7 @@ export function CommitGraph(props: CommitGraphProps) {
265266

266267
const fetchCommitHistory = async () => {
267268
setLoading(true);
269+
setError(null);
268270
try {
269271
const projectPath = getProjectPath();
270272

@@ -304,6 +306,7 @@ export function CommitGraph(props: CommitGraphProps) {
304306
setCommits(mappedCommits);
305307
} catch (err) {
306308
console.error("Failed to fetch commit history:", err);
309+
setError(`Failed to load commit history: ${err}`);
307310
} finally {
308311
setLoading(false);
309312
}
@@ -678,6 +681,20 @@ export function CommitGraph(props: CommitGraphProps) {
678681
</div>
679682
</div>
680683

684+
{/* Error Banner */}
685+
<Show when={error()}>
686+
<div
687+
class="flex items-center gap-2 px-3 py-2 text-sm"
688+
style={{ background: "var(--status-error-bg, rgba(239,68,68,0.1))", color: "var(--status-error, #ef4444)" }}
689+
>
690+
<Icon name="circle-exclamation" class="w-4 h-4 shrink-0" />
691+
<span class="flex-1 truncate">{error()}</span>
692+
<button class="p-0.5 rounded hover:bg-white/10" onClick={() => setError(null)}>
693+
<Icon name="xmark" class="w-3.5 h-3.5" />
694+
</button>
695+
</div>
696+
</Show>
697+
681698
{/* Search */}
682699
<div class="px-3 py-2 border-b" style={{ "border-color": "var(--border-weak)" }}>
683700
<div

src/components/git/DiffView.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function DiffView(props: DiffViewProps) {
5757

5858
const [diff, setDiff] = createSignal<FileDiff | null>(null);
5959
const [loading, setLoading] = createSignal(false);
60+
const [error, setError] = createSignal<string | null>(null);
6061
const [viewMode, setViewMode] = createSignal<"unified" | "split">("unified");
6162
const [isFullscreen, setIsFullscreen] = createSignal(false);
6263
const [copied, setCopied] = createSignal(false);
@@ -111,12 +112,17 @@ export function DiffView(props: DiffViewProps) {
111112

112113
const fetchDiff = async (file: string, staged: boolean) => {
113114
setLoading(true);
115+
setError(null);
114116
try {
115117
const diffText = await gitDiff(getProjectPath(), file, staged);
116118
const rawDiff: RawFileDiff = { path: file, content: diffText, hunks: [], additions: 0, deletions: 0 };
117119
setDiff(rawDiff);
118-
} catch (err) { console.error("Failed to fetch diff:", err); }
119-
finally { setLoading(false); }
120+
} catch (err) {
121+
console.error("Failed to fetch diff:", err);
122+
setError(`Failed to load diff: ${err}`);
123+
} finally {
124+
setLoading(false);
125+
}
120126
};
121127

122128
const handleHunkAction = async (
@@ -260,6 +266,18 @@ export function DiffView(props: DiffViewProps) {
260266
onCopyDiff={copyDiff} onEnterEditMode={handleEnterEditMode}
261267
onSaveEdit={handleSaveEdit} onCancelEdit={handleCancelEdit} onClose={props.onClose}
262268
/>
269+
<Show when={error()}>
270+
<div
271+
class="flex items-center gap-2 px-3 py-2 text-sm"
272+
style={{ background: "var(--status-error-bg, rgba(239,68,68,0.1))", color: "var(--status-error, #ef4444)" }}
273+
>
274+
<Icon name="circle-exclamation" class="w-4 h-4 shrink-0" />
275+
<span class="flex-1 truncate">{error()}</span>
276+
<button class="p-0.5 rounded hover:bg-white/10" onClick={() => setError(null)}>
277+
<Icon name="xmark" class="w-3.5 h-3.5" />
278+
</button>
279+
</div>
280+
</Show>
263281
<Show when={editMode()} fallback={
264282
<div class="flex-1 overflow-auto font-mono text-sm">
265283
<Show when={loading()}>

src/components/git/GitPanel.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,10 +362,13 @@ const [signCommits, setSignCommits] = createSignal(false);
362362

363363
const clearError = () => setError(null);
364364

365+
let errorTimeout: ReturnType<typeof setTimeout> | undefined;
365366
const showError = (message: string, type: "error" | "warning" = "error") => {
366367
setError({ message, type });
367-
setTimeout(clearError, 5000);
368+
if (errorTimeout) clearTimeout(errorTimeout);
369+
errorTimeout = setTimeout(clearError, 5000);
368370
};
371+
onCleanup(() => { if (errorTimeout) clearTimeout(errorTimeout); });
369372

370373
const fetchCommits = async (repoPath: string) => {
371374
try {

0 commit comments

Comments
 (0)