* feat: show last commit hash in status bar, remove branch name Replace the hardcoded "main" branch display with a clickable short SHA linking to the GitHub commit. Add Rust get_last_commit_info command that returns the last commit hash and constructs a GitHub URL from the remote. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add design/git-status-bar.pen with new status bar layout Shows the updated status bar with commit hash link, no branch name, and annotated change callouts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: retrigger — runner 3 pnpm not found (env issue) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { isTauri, mockInvoke } from '../mock-tauri'
|
|
import type { GitPullResult, LastCommitInfo, SyncStatus } from '../types'
|
|
|
|
const DEFAULT_INTERVAL_MS = 5 * 60_000
|
|
|
|
function tauriCall<T>(cmd: string, args: Record<string, unknown>): Promise<T> {
|
|
return isTauri() ? invoke<T>(cmd, args) : mockInvoke<T>(cmd, args)
|
|
}
|
|
|
|
interface UseAutoSyncOptions {
|
|
vaultPath: string
|
|
intervalMinutes: number | null
|
|
onVaultUpdated: () => void
|
|
onConflict: (files: string[]) => void
|
|
onToast: (msg: string) => void
|
|
}
|
|
|
|
export interface AutoSyncState {
|
|
syncStatus: SyncStatus
|
|
lastSyncTime: number | null
|
|
conflictFiles: string[]
|
|
lastCommitInfo: LastCommitInfo | null
|
|
triggerSync: () => void
|
|
}
|
|
|
|
export function useAutoSync({
|
|
vaultPath,
|
|
intervalMinutes,
|
|
onVaultUpdated,
|
|
onConflict,
|
|
onToast,
|
|
}: UseAutoSyncOptions): AutoSyncState {
|
|
const [syncStatus, setSyncStatus] = useState<SyncStatus>('idle')
|
|
const [lastSyncTime, setLastSyncTime] = useState<number | null>(null)
|
|
const [conflictFiles, setConflictFiles] = useState<string[]>([])
|
|
const [lastCommitInfo, setLastCommitInfo] = useState<LastCommitInfo | null>(null)
|
|
const syncingRef = useRef(false)
|
|
const callbacksRef = useRef({ onVaultUpdated, onConflict, onToast })
|
|
callbacksRef.current = { onVaultUpdated, onConflict, onToast }
|
|
|
|
const performPull = useCallback(async () => {
|
|
if (syncingRef.current) return
|
|
syncingRef.current = true
|
|
setSyncStatus('syncing')
|
|
|
|
try {
|
|
const result = await tauriCall<GitPullResult>('git_pull', { vaultPath })
|
|
setLastSyncTime(Date.now())
|
|
tauriCall<LastCommitInfo | null>('get_last_commit_info', { vaultPath })
|
|
.then(info => setLastCommitInfo(info))
|
|
.catch(() => {})
|
|
|
|
if (result.status === 'updated') {
|
|
setSyncStatus('idle')
|
|
setConflictFiles([])
|
|
callbacksRef.current.onVaultUpdated()
|
|
callbacksRef.current.onToast(`Pulled ${result.updatedFiles.length} update(s) from remote`)
|
|
} else if (result.status === 'conflict') {
|
|
setSyncStatus('conflict')
|
|
setConflictFiles(result.conflictFiles)
|
|
callbacksRef.current.onConflict(result.conflictFiles)
|
|
} else if (result.status === 'error') {
|
|
setSyncStatus('error')
|
|
} else {
|
|
// up_to_date or no_remote
|
|
setSyncStatus('idle')
|
|
setConflictFiles([])
|
|
}
|
|
} catch {
|
|
setSyncStatus('error')
|
|
setLastSyncTime(Date.now())
|
|
} finally {
|
|
syncingRef.current = false
|
|
}
|
|
}, [vaultPath])
|
|
|
|
// Pull on mount (app launch)
|
|
useEffect(() => {
|
|
performPull()
|
|
}, [performPull])
|
|
|
|
// Pull on window focus (app foreground)
|
|
useEffect(() => {
|
|
const handleFocus = () => { performPull() }
|
|
window.addEventListener('focus', handleFocus)
|
|
return () => window.removeEventListener('focus', handleFocus)
|
|
}, [performPull])
|
|
|
|
// Periodic pull
|
|
useEffect(() => {
|
|
const ms = (intervalMinutes ?? 5) * 60_000 || DEFAULT_INTERVAL_MS
|
|
const id = setInterval(performPull, ms)
|
|
return () => clearInterval(id)
|
|
}, [performPull, intervalMinutes])
|
|
|
|
return { syncStatus, lastSyncTime, conflictFiles, lastCommitInfo, triggerSync: performPull }
|
|
}
|