Skip to content
Merged
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
28 changes: 28 additions & 0 deletions wimygit-tauri/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ import {
type LfsLock,
} from "./lib";
import { LfsUnlockModal } from "./components/shared/LfsUnlockModal";
import {
loadAutoFetchSettings,
saveAutoFetchSettings,
type AutoFetchSettings,
} from "./hooks/useAutoFetch";

const BASE_INNER_TABS = [
{ id: "pending", label: "Pending Changes" },
Expand All @@ -47,6 +52,7 @@ interface RepoTabState {
repoName: string;
activeTab: string;
refreshKey: number;
silentRefreshKey: number;
}

const STORAGE_KEY = "repoTabs_v2";
Expand Down Expand Up @@ -90,6 +96,7 @@ function App() {
const [lfsLockCount, setLfsLockCount] = useState(0);
const [worktreeCount, setWorktreeCount] = useState(0);
const [showPluginModal, setShowPluginModal] = useState(false);
const [autoFetchSettings, setAutoFetchSettings] = useState<AutoFetchSettings>(loadAutoFetchSettings);
const [lfsUnlockConfirm, setLfsUnlockConfirm] = useState<{
repoPath: string;
locks: LfsLock[];
Expand All @@ -114,6 +121,7 @@ function App() {
repoName: s.repoName,
activeTab: "pending",
refreshKey: 0,
silentRefreshKey: 0,
}));
setRepoTabs(tabs);
const lastActive = localStorage.getItem("activeRepoId");
Expand Down Expand Up @@ -159,6 +167,14 @@ function App() {
updateActiveRepo((t) => ({ refreshKey: t.refreshKey + 1 }));
}, [updateActiveRepo]);

const handleSilentRefresh = useCallback(() => {
setRepoTabs((prev) =>
prev.map((t) =>
t.id === activeRepoId ? { ...t, silentRefreshKey: t.silentRefreshKey + 1 } : t
)
);
}, [activeRepoId]);

const handleAfterPush = useCallback(async (repoPath: string) => {
try {
const myLocks = await getLfsLocalLocks(repoPath);
Expand All @@ -172,6 +188,11 @@ function App() {
}
}, []);

const handleAutoFetchSettingsChange = useCallback((settings: AutoFetchSettings) => {
saveAutoFetchSettings(settings);
setAutoFetchSettings(settings);
}, []);

const handleLfsUnlockConfirm = useCallback(async () => {
if (!lfsUnlockConfirm) return;
const { repoPath, locks } = lfsUnlockConfirm;
Expand Down Expand Up @@ -290,6 +311,7 @@ function App() {
repoName: repoNameFromPath(root),
activeTab: "pending",
refreshKey: 0,
silentRefreshKey: 0,
};

setRepoTabs((prev) => {
Expand Down Expand Up @@ -429,6 +451,9 @@ function App() {
plugins={plugins}
selectedFilePath={selectedFilePath}
onTimeLapse={() => setShowTimeLapse(true)}
autoFetchSettings={autoFetchSettings}
onAutoFetchSettingsChange={handleAutoFetchSettingsChange}
onSilentRefresh={handleSilentRefresh}
/>

{/* Body: left sidebar + main content */}
Expand Down Expand Up @@ -466,6 +491,7 @@ function App() {
<PendingTab
repoPath={activeRepo.repoPath}
refreshKey={activeRepo.refreshKey}
silentRefreshKey={activeRepo.silentRefreshKey}
onFilePreview={(filename, staged) => { setSelectedDiff(null); setPendingFilePreview({ filename, staged }); }}
onLfsLockCountChange={setLfsLockCount}
onShowInWorkspaceFile={(absolutePath) => {
Expand All @@ -490,6 +516,7 @@ function App() {
repoPath={activeRepo.repoPath}
filePath={selectedFilePath}
refreshKey={activeRepo.refreshKey}
silentRefreshKey={activeRepo.silentRefreshKey}
onRefresh={handleRefresh}
onFileSelect={setSelectedDiff}
onClearPath={() => setSelectedFilePath(null)}
Expand All @@ -511,6 +538,7 @@ function App() {
<BranchTab
repoPath={activeRepo.repoPath}
refreshKey={activeRepo.refreshKey}
silentRefreshKey={activeRepo.silentRefreshKey}
onRefresh={handleRefresh}
/>
)}
Expand Down
28 changes: 27 additions & 1 deletion wimygit-tauri/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
type PluginInfo,
} from "../../lib";
import { PluginButtons } from "./PluginButtons";
import { useAutoFetch, type AutoFetchSettings } from "../../hooks/useAutoFetch";
import { AutoFetchIndicator } from "../shared/AutoFetchIndicator";

interface HeaderProps {
repoPath: string;
Expand All @@ -21,6 +23,9 @@ interface HeaderProps {
plugins?: PluginInfo[];
selectedFilePath?: string | null;
onTimeLapse?: () => void;
autoFetchSettings: AutoFetchSettings;
onAutoFetchSettingsChange: (settings: AutoFetchSettings) => void;
onSilentRefresh: () => void;
}

type BusyKey =
Expand Down Expand Up @@ -208,7 +213,7 @@ function Sep() {

// ─── Header ───────────────────────────────────────────────────────────────────

export function Header({ repoPath, refreshKey, onRefresh, onPushSuccess, plugins = [], selectedFilePath, onTimeLapse }: HeaderProps) {
export function Header({ repoPath, refreshKey, onRefresh, onPushSuccess, plugins = [], selectedFilePath, onTimeLapse, autoFetchSettings, onAutoFetchSettingsChange, onSilentRefresh }: HeaderProps) {
const [currentBranch, setCurrentBranch] = useState<string>("");
const [repoName, setRepoName] = useState<string>("");
const [author, setAuthor] = useState<{ name: string; email: string } | null>(null);
Expand Down Expand Up @@ -252,6 +257,18 @@ export function Header({ repoPath, refreshKey, onRefresh, onPushSuccess, plugins

const isDisabled = busy !== null;

const handleAutoFetch = useCallback(async () => {
await gitFetchAll(repoPath);
onSilentRefresh();
}, [repoPath, onSilentRefresh]);

const { lastFetchedAt, nextFetchIn, isFetching: isAutoFetching } = useAutoFetch({
settings: autoFetchSettings,
repoPath,
isBusy: busy !== null,
onFetch: handleAutoFetch,
});

// ── Push dropdown items ──────────────────────────────────────────────────

const pushItems: DropdownItem[] = [
Expand Down Expand Up @@ -332,6 +349,15 @@ export function Header({ repoPath, refreshKey, onRefresh, onPushSuccess, plugins
onClick={() => run("fetchAll", () => gitFetchAll(repoPath), true)}
/>

{/* ── 3b. Auto Fetch indicator ── */}
<AutoFetchIndicator
settings={autoFetchSettings}
nextFetchIn={nextFetchIn}
isFetching={isAutoFetching}
lastFetchedAt={lastFetchedAt}
onChange={onAutoFetchSettingsChange}
/>

{/* ── 4. Pull ── */}
<ToolButton
icon={<IconPull />}
Expand Down
112 changes: 112 additions & 0 deletions wimygit-tauri/src/components/shared/AutoFetchIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useState, useRef, useEffect } from "react";
import { AUTO_FETCH_INTERVALS, type AutoFetchSettings } from "../../hooks/useAutoFetch";

interface AutoFetchIndicatorProps {
settings: AutoFetchSettings;
nextFetchIn: number;
isFetching: boolean;
lastFetchedAt: Date | null;
onChange: (settings: AutoFetchSettings) => void;
}

function formatCountdown(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${String(s).padStart(2, "0")}`;
}

export function AutoFetchIndicator({
settings,
nextFetchIn,
isFetching,
lastFetchedAt,
onChange,
}: AutoFetchIndicatorProps) {
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [open]);

const label = settings.enabled
? isFetching
? "Fetching…"
: formatCountdown(nextFetchIn)
: "Auto";

const tooltip = settings.enabled
? `Auto-fetch every ${settings.intervalMinutes}m — click to configure`
: "Auto-fetch disabled — click to enable";

return (
<div ref={wrapRef} className="relative shrink-0">
<button
onClick={() => setOpen((v) => !v)}
title={tooltip}
className={`flex items-center gap-1 px-2 py-1 h-full text-[10px] rounded border transition-colors ${
settings.enabled
? "bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700 text-green-700 dark:text-green-300"
: "bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-500"
}`}
>
<span
className={isFetching ? "inline-block animate-spin" : ""}
style={{ display: "inline-block" }}
>
</span>
<span className="tabular-nums">{label}</span>
</button>

{open && (
<div className="absolute left-0 top-full mt-0.5 z-50 w-52 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded shadow-lg p-3">
<div className="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
Auto Fetch
</div>

<label className="flex items-center gap-2 mb-3 cursor-pointer select-none">
<input
type="checkbox"
checked={settings.enabled}
onChange={(e) => onChange({ ...settings, enabled: e.target.checked })}
className="w-3.5 h-3.5 accent-blue-500"
/>
<span className="text-xs text-gray-600 dark:text-gray-400">Enable auto-fetch</span>
</label>

<div className="text-[10px] text-gray-500 dark:text-gray-500 mb-1.5">Interval</div>
<div className="flex flex-wrap gap-1 mb-2">
{AUTO_FETCH_INTERVALS.map((m) => (
<button
key={m}
onClick={() => onChange({ ...settings, intervalMinutes: m })}
disabled={!settings.enabled}
className={`px-2 py-0.5 text-[10px] rounded border transition-colors disabled:opacity-40 ${
settings.intervalMinutes === m
? "bg-blue-500 border-blue-500 text-white"
: "bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600"
}`}
>
{m}m
</button>
))}
</div>

{lastFetchedAt && (
<div className="text-[10px] text-gray-400 dark:text-gray-500">
Last fetched: {lastFetchedAt.toLocaleTimeString()}
</div>
)}
</div>
)}
</div>
);
}
23 changes: 22 additions & 1 deletion wimygit-tauri/src/components/tabs/BranchTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
interface BranchTabProps {
repoPath: string;
refreshKey: number;
silentRefreshKey?: number;
onRefresh: () => void;
}

export function BranchTab({ repoPath, refreshKey, onRefresh }: BranchTabProps) {
export function BranchTab({ repoPath, refreshKey, silentRefreshKey, onRefresh }: BranchTabProps) {
const [branches, setBranches] = useState<BranchInfo[]>([]);
const [currentBranch, setCurrentBranch] = useState<string>("");
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -51,6 +52,26 @@ export function BranchTab({ repoPath, refreshKey, onRefresh }: BranchTabProps) {
return () => { fetchGenRef.current++; };
}, [repoPath, refreshKey]);

const silentFetchGenRef = useRef(0);

useEffect(() => {
if (!silentRefreshKey || !repoPath) return;
const gen = ++silentFetchGenRef.current;
(async () => {
try {
const [branchList, current] = await Promise.all([
getBranches(repoPath),
getCurrentBranch(repoPath),
]);
if (gen !== silentFetchGenRef.current) return;
setBranches(branchList);
setCurrentBranch(current);
} catch {
// silent refresh errors are ignored
}
})();
}, [repoPath, silentRefreshKey]);

const handleCheckout = async (branchName: string) => {
try {
await gitCheckout(repoPath, branchName);
Expand Down
20 changes: 19 additions & 1 deletion wimygit-tauri/src/components/tabs/HistoryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface HistoryTabProps {
repoPath: string;
filePath?: string | null;
refreshKey: number;
silentRefreshKey?: number;
onRefresh: () => void;
onFileSelect?: (info: SelectedDiffInfo) => void;
onClearPath?: () => void;
Expand Down Expand Up @@ -240,7 +241,7 @@ function FileContextMenu({ x, y, absolutePath, onClose, onShowInWorkspace, onSho

const PAGE_SIZE = 100;

export function HistoryTab({ repoPath, filePath, refreshKey, onRefresh, onFileSelect, onClearPath, onShowInWorkspace, onShowInWorkspaceFile, onShowInHistoryFile }: HistoryTabProps) {
export function HistoryTab({ repoPath, filePath, refreshKey, silentRefreshKey, onRefresh, onFileSelect, onClearPath, onShowInWorkspace, onShowInWorkspaceFile, onShowInHistoryFile }: HistoryTabProps) {
const [commits, setCommits] = useState<CommitInfo[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
Expand Down Expand Up @@ -295,6 +296,23 @@ export function HistoryTab({ repoPath, filePath, refreshKey, onRefresh, onFileSe
return () => { loadHistoryGenRef.current++; };
}, [repoPath, refreshKey, loadHistory]);

const silentHistoryGenRef = useRef(0);

useEffect(() => {
if (!silentRefreshKey || !repoPath) return;
const gen = ++silentHistoryGenRef.current;
(async () => {
try {
const result = await getHistory(repoPath, filePath ?? "", 0, PAGE_SIZE, allBranches, searchQuery || undefined);
if (gen !== silentHistoryGenRef.current) return;
setCommits(result);
setHasMore(result.length === PAGE_SIZE);
} catch {
// silent refresh errors are ignored
}
})();
}, [repoPath, silentRefreshKey]);

useEffect(() => {
if (!repoPath) return;
let cancelled = false;
Expand Down
Loading
Loading