{remoteStatus?.branch || '—'}
@@ -638,6 +657,9 @@ export function SyncBadge({
status,
lastSyncTime,
remoteStatus,
+ repositories,
+ selectedRepositoryPath,
+ onRepositoryChange,
onTriggerSync,
onPullAndPush,
onOpenConflictResolver,
@@ -647,6 +669,9 @@ export function SyncBadge({
status: SyncStatus
lastSyncTime: number | null
remoteStatus?: GitRemoteStatus | null
+ repositories?: GitRepositoryOption[]
+ selectedRepositoryPath?: string
+ onRepositoryChange?: (path: string) => void
onTriggerSync?: () => void
onPullAndPush?: () => void
onOpenConflictResolver?: () => void
@@ -656,6 +681,9 @@ export function SyncBadge({
const [showPopup, setShowPopup] = useState(false)
const popupRef = useRef
(null)
const isSyncing = status === 'syncing'
+ const selectedRepositoryLabel = selectedRepositoryPath && repositories
+ ? gitRepositoryLabel(selectedRepositoryPath, repositories)
+ : null
useDismissibleLayer(showPopup, popupRef, () => setShowPopup(false))
@@ -678,13 +706,16 @@ export function SyncBadge({
- {compact ? null : formatSyncLabel(locale, status, lastSyncTime)}
+ {compact ? null : formatSyncBadgeLabel(locale, status, lastSyncTime, selectedRepositoryLabel)}
{showPopup && (
setShowPopup(false)}
diff --git a/src/components/status-bar/StatusBarSections.tsx b/src/components/status-bar/StatusBarSections.tsx
index 738e918a..040d129d 100644
--- a/src/components/status-bar/StatusBarSections.tsx
+++ b/src/components/status-bar/StatusBarSections.tsx
@@ -31,6 +31,7 @@ import { ICON_STYLE, SEP_STYLE } from './styles'
import type { VaultOption } from './types'
import { VaultMenu } from './VaultMenu'
import { formatShortcutDisplay } from '../../hooks/appCommandCatalog'
+import type { GitRepositoryOption } from '../../utils/gitRepositories'
const SETTINGS_SHORTCUT = {
shortcut: formatShortcutDisplay({ display: '⌘,' }),
@@ -66,6 +67,9 @@ interface StatusBarPrimarySectionProps {
lastSyncTime: number | null
conflictCount: number
remoteStatus?: GitRemoteStatus | null
+ repositories?: GitRepositoryOption[]
+ selectedRepositoryPath?: string
+ onRepositoryChange?: (path: string) => void
onTriggerSync?: () => void
onPullAndPush?: () => void
onOpenConflictResolver?: () => void
@@ -194,6 +198,9 @@ function StatusBarAiBadge({
function StatusBarPrimaryBadges({
modifiedCount,
visibleRemoteStatus,
+ repositories,
+ selectedRepositoryPath,
+ onRepositoryChange,
onAddRemote,
onClickPending,
onCommitPush,
@@ -227,6 +234,9 @@ function StatusBarPrimaryBadges({
}: {
modifiedCount: number
visibleRemoteStatus: GitRemoteStatus | null
+ repositories?: GitRepositoryOption[]
+ selectedRepositoryPath?: string
+ onRepositoryChange?: (path: string) => void
onAddRemote: () => void
onClickPending?: () => void
onCommitPush?: () => void
@@ -271,6 +281,9 @@ function StatusBarPrimaryBadges({
status={syncStatus}
lastSyncTime={lastSyncTime}
remoteStatus={visibleRemoteStatus}
+ repositories={repositories}
+ selectedRepositoryPath={selectedRepositoryPath}
+ onRepositoryChange={onRepositoryChange}
onTriggerSync={onTriggerSync}
onPullAndPush={onPullAndPush}
onOpenConflictResolver={onOpenConflictResolver}
@@ -408,6 +421,103 @@ function PrimarySeparator({ compact }: { compact: boolean }) {
return compact ? null : |
}
+function StatusBarGitControls({
+ modifiedCount,
+ vaultPath,
+ onAddRemote,
+ onClickPending,
+ onCommitPush,
+ commitActionPending,
+ gitFeaturesEnabled,
+ onInitializeGit,
+ isOffline,
+ isVaultReloading,
+ isGitVault,
+ syncStatus,
+ lastSyncTime,
+ conflictCount,
+ remoteStatus,
+ repositories,
+ selectedRepositoryPath,
+ onRepositoryChange,
+ onTriggerSync,
+ onPullAndPush,
+ onOpenConflictResolver,
+ onClickPulse,
+ mcpStatus,
+ onInstallMcp,
+ aiAgentsStatus,
+ vaultAiGuidanceStatus,
+ defaultAiAgent,
+ defaultAiTarget,
+ aiModelProviders,
+ onSetDefaultAiAgent,
+ onSetDefaultAiTarget,
+ onRestoreVaultAiGuidance,
+ claudeCodeStatus,
+ claudeCodeVersion,
+ compact,
+ locale,
+}: StatusBarPrimarySectionProps & { compact: boolean; locale: AppLocale }) {
+ const gitVaultPath = selectedRepositoryPath || vaultPath
+ const { openAddRemote, closeAddRemote, showAddRemote, visibleRemoteStatus, handleRemoteConnected } = useStatusBarAddRemote({
+ vaultPath: gitVaultPath,
+ isGitVault: gitFeaturesEnabled !== false && isGitVault !== false,
+ remoteStatus,
+ onAddRemote,
+ })
+
+ return (
+ <>
+ {
+ void openAddRemote()
+ }}
+ onClickPending={onClickPending}
+ onCommitPush={onCommitPush}
+ commitActionPending={commitActionPending}
+ gitFeaturesEnabled={gitFeaturesEnabled !== false}
+ onInitializeGit={onInitializeGit}
+ syncStatus={syncStatus}
+ lastSyncTime={lastSyncTime}
+ onTriggerSync={onTriggerSync}
+ onPullAndPush={onPullAndPush}
+ onOpenConflictResolver={onOpenConflictResolver}
+ conflictCount={conflictCount}
+ onClickPulse={onClickPulse}
+ isGitVault={isGitVault !== false}
+ mcpStatus={mcpStatus}
+ onInstallMcp={onInstallMcp}
+ aiAgentsStatus={aiAgentsStatus}
+ vaultAiGuidanceStatus={vaultAiGuidanceStatus}
+ defaultAiAgent={defaultAiAgent}
+ defaultAiTarget={defaultAiTarget}
+ aiModelProviders={aiModelProviders}
+ onSetDefaultAiAgent={onSetDefaultAiAgent}
+ onSetDefaultAiTarget={onSetDefaultAiTarget}
+ onRestoreVaultAiGuidance={onRestoreVaultAiGuidance}
+ claudeCodeStatus={claudeCodeStatus}
+ claudeCodeVersion={claudeCodeVersion}
+ isOffline={isOffline === true}
+ isVaultReloading={isVaultReloading === true}
+ compact={compact}
+ locale={locale}
+ />
+
+ >
+ )
+}
+
export function StatusBarPrimarySection({
modifiedCount,
vaultPath,
@@ -430,6 +540,9 @@ export function StatusBarPrimarySection({
lastSyncTime,
conflictCount,
remoteStatus,
+ repositories,
+ selectedRepositoryPath,
+ onRepositoryChange,
onTriggerSync,
onPullAndPush,
onOpenConflictResolver,
@@ -454,13 +567,6 @@ export function StatusBarPrimarySection({
stacked = false,
compact = false,
}: StatusBarPrimarySectionProps) {
- const { openAddRemote, closeAddRemote, showAddRemote, visibleRemoteStatus, handleRemoteConnected } = useStatusBarAddRemote({
- vaultPath,
- isGitVault: gitFeaturesEnabled && isGitVault,
- remoteStatus,
- onAddRemote,
- })
-
return (
-
{
- void openAddRemote()
- }}
+ vaultPath={vaultPath}
+ vaults={vaults}
+ onSwitchVault={onSwitchVault}
+ remoteStatus={remoteStatus}
+ repositories={repositories}
+ selectedRepositoryPath={selectedRepositoryPath}
+ onRepositoryChange={onRepositoryChange}
+ onAddRemote={onAddRemote}
onClickPending={onClickPending}
onCommitPush={onCommitPush}
commitActionPending={commitActionPending}
@@ -516,12 +626,6 @@ export function StatusBarPrimarySection({
compact={compact}
locale={locale}
/>
-
)
}
diff --git a/src/hooks/commands/gitCommands.ts b/src/hooks/commands/gitCommands.ts
index 9ad67581..9a523e3d 100644
--- a/src/hooks/commands/gitCommands.ts
+++ b/src/hooks/commands/gitCommands.ts
@@ -1,29 +1,65 @@
import type { CommandAction } from './types'
import type { SidebarSelection } from '../../types'
+import type { GitRepositoryOption } from '../../utils/gitRepositories'
interface GitCommandsConfig {
modifiedCount: number
canAddRemote: boolean
gitFeaturesEnabled?: boolean
isGitVault?: boolean
+ repositories?: GitRepositoryOption[]
onAddRemote?: () => void
onCommitPush: () => void
onInitializeGit?: () => void
onPull?: () => void
+ onPullRepository?: (path: string) => void
onResolveConflicts?: () => void
onSelect: (sel: SidebarSelection) => void
}
+type PullableRepositoryList = [GitRepositoryOption, GitRepositoryOption, ...GitRepositoryOption[]]
+
+function hasRepositoryPullTargets(
+ repositories: GitRepositoryOption[] | undefined,
+): repositories is PullableRepositoryList {
+ if (!repositories) return false
+ return repositories.length > 1
+}
+
+function buildPullCommands({
+ onPull,
+ onPullRepository,
+ repositories,
+}: Pick): CommandAction[] {
+ if (onPullRepository && hasRepositoryPullTargets(repositories)) {
+ const pullRepository = onPullRepository
+ return repositories.map((repository, index) => ({
+ id: `git-pull-${index}`,
+ label: `Pull from Remote: ${repository.label}`,
+ group: 'Git',
+ keywords: ['git', 'pull', 'fetch', 'download', 'sync', 'remote', repository.label, repository.path],
+ enabled: true,
+ execute: () => pullRepository(repository.path),
+ }))
+ }
+
+ return [
+ { id: 'git-pull', label: 'Pull from Remote', group: 'Git', keywords: ['git', 'pull', 'fetch', 'download', 'sync', 'remote'], enabled: true, execute: () => onPull?.() },
+ ]
+}
+
export function buildGitCommands(config: GitCommandsConfig): CommandAction[] {
const {
modifiedCount,
canAddRemote,
gitFeaturesEnabled = true,
isGitVault = true,
+ repositories,
onAddRemote,
onCommitPush,
onInitializeGit,
onPull,
+ onPullRepository,
onResolveConflicts,
onSelect,
} = config
@@ -46,7 +82,7 @@ export function buildGitCommands(config: GitCommandsConfig): CommandAction[] {
return [
{ id: 'commit-push', label: 'Commit & Push', group: 'Git', keywords: ['git', 'save', 'sync'], enabled: modifiedCount > 0, execute: onCommitPush },
{ id: 'add-remote', label: 'Add Remote to Current Vault', group: 'Git', keywords: ['git', 'remote', 'connect', 'origin', 'no remote'], enabled: canAddRemote && !!onAddRemote, execute: () => onAddRemote?.() },
- { id: 'git-pull', label: 'Pull from Remote', group: 'Git', keywords: ['git', 'pull', 'fetch', 'download', 'sync', 'remote'], enabled: true, execute: () => onPull?.() },
+ ...buildPullCommands({ onPull, onPullRepository, repositories }),
{ id: 'resolve-conflicts', label: 'Resolve Conflicts', group: 'Git', keywords: ['conflict', 'merge', 'git', 'sync'], enabled: true, execute: () => onResolveConflicts?.() },
{ id: 'view-changes', label: 'View Pending Changes', group: 'Git', keywords: ['modified', 'diff'], enabled: true, execute: () => onSelect({ kind: 'filter', filter: 'changes' }) },
]
diff --git a/src/hooks/commands/localizeCommands.ts b/src/hooks/commands/localizeCommands.ts
index 8ce0049d..f973c765 100644
--- a/src/hooks/commands/localizeCommands.ts
+++ b/src/hooks/commands/localizeCommands.ts
@@ -159,6 +159,16 @@ function localizeSettingsStateCommand(command: CommandAction, t: Translate): str
return null
}
+function localizeGitStateCommand(command: CommandAction, t: Translate): string | null {
+ if (command.id.startsWith('git-pull-')) {
+ return t('command.git.pullRepository', {
+ repository: stripKnownPrefix(command.label, 'Pull from Remote: '),
+ })
+ }
+
+ return null
+}
+
function localizeTypeCommand(command: CommandAction, t: Translate): string | null {
if (command.id.startsWith('new-') && command.group === 'Note') {
return t('command.note.newTypedNote', { type: stripKnownPrefix(command.label, 'New ') })
@@ -184,6 +194,7 @@ export function localizeCommandActions(commands: CommandAction[], locale: AppLoc
: localizeNoteStateCommand(command, t)
?? localizeViewStateCommand(command, t)
?? localizeSettingsStateCommand(command, t)
+ ?? localizeGitStateCommand(command, t)
?? localizeTypeCommand(command, t)
?? command.label
return label === command.label ? command : { ...command, label }
diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts
index cef285d6..1fc508ec 100644
--- a/src/hooks/useAppCommands.ts
+++ b/src/hooks/useAppCommands.ts
@@ -13,6 +13,7 @@ import { requestAddRemote } from '../utils/addRemoteEvents'
import type { NoteListFilter } from '../utils/noteListHelpers'
import type { ViewMode } from './useViewMode'
import type { NoteListMultiSelectionCommands } from '../components/note-list/multiSelectionCommands'
+import type { GitRepositoryOption } from '../utils/gitRepositories'
interface AppCommandsConfig {
activeTabPath: string | null
@@ -38,6 +39,7 @@ interface AppCommandsConfig {
onUnarchiveNote: (path: string) => void
onCommitPush: () => void
onPull?: () => void
+ onPullRepository?: (path: string) => void
onResolveConflicts?: () => void
onSetViewMode: (mode: ViewMode) => void
onToggleInspector: () => void
@@ -73,6 +75,7 @@ interface AppCommandsConfig {
canAddRemote?: boolean
gitFeaturesEnabled?: boolean
isGitVault?: boolean
+ gitRepositories?: GitRepositoryOption[]
onInitializeGit?: () => void
onCreateType?: () => void
aiFeaturesEnabled?: boolean
@@ -164,6 +167,7 @@ type CommandRegistryCoreActions = Pick<
| 'onUnarchiveNote'
| 'onCommitPush'
| 'onPull'
+ | 'onPullRepository'
| 'onResolveConflicts'
| 'onSetViewMode'
| 'onToggleInspector'
@@ -189,6 +193,7 @@ type CommandRegistryVaultActions = Pick<
| 'canAddRemote'
| 'gitFeaturesEnabled'
| 'isGitVault'
+ | 'gitRepositories'
| 'onInitializeGit'
| 'onCheckForUpdates'
| 'onCreateType'
@@ -451,6 +456,7 @@ function createCommandRegistryCoreConfig(
onUnarchiveNote: config.onUnarchiveNote,
onCommitPush: config.onCommitPush,
onPull: config.onPull,
+ onPullRepository: config.onPullRepository,
onResolveConflicts: config.onResolveConflicts,
onSetViewMode: config.onSetViewMode,
onToggleInspector: config.onToggleInspector,
@@ -483,6 +489,7 @@ function createCommandRegistryVaultConfig(
canAddRemote: config.canAddRemote ?? true,
gitFeaturesEnabled: config.gitFeaturesEnabled,
isGitVault: config.isGitVault,
+ gitRepositories: config.gitRepositories,
onInitializeGit: config.onInitializeGit,
onCheckForUpdates: config.onCheckForUpdates,
onCreateType: config.onCreateType,
diff --git a/src/hooks/useAutoSync.extra.test.ts b/src/hooks/useAutoSync.extra.test.ts
index 4cf03956..cc2bfd87 100644
--- a/src/hooks/useAutoSync.extra.test.ts
+++ b/src/hooks/useAutoSync.extra.test.ts
@@ -120,7 +120,7 @@ describe('useAutoSync extra', () => {
})
await waitFor(() => {
- expect(hook.onVaultUpdated).toHaveBeenCalledWith(['notes/today.md'])
+ expect(hook.onVaultUpdated).toHaveBeenCalledWith(['notes/today.md'], '/Users/luca/Laputa')
expect(hook.onSyncUpdated).toHaveBeenCalledOnce()
expect(hook.onToast).toHaveBeenCalledWith('Pulled and pushed successfully')
expect(hook.result.current.syncStatus).toBe('idle')
diff --git a/src/hooks/useAutoSync.test.ts b/src/hooks/useAutoSync.test.ts
index 398ade88..807c239b 100644
--- a/src/hooks/useAutoSync.test.ts
+++ b/src/hooks/useAutoSync.test.ts
@@ -96,7 +96,7 @@ describe('useAutoSync', () => {
const { result } = renderSync()
await waitFor(() => {
- expect(onVaultUpdated).toHaveBeenCalledWith(['note.md', 'project/plan.md'])
+ expect(onVaultUpdated).toHaveBeenCalledWith(['note.md', 'project/plan.md'], '/Users/luca/Laputa')
expect(onToast).toHaveBeenCalledWith('Pulled 2 update(s) from remote')
expect(result.current.syncStatus).toBe('idle')
})
@@ -124,7 +124,7 @@ describe('useAutoSync', () => {
)
await waitFor(() => {
- expect(asyncVaultRefresh).toHaveBeenCalledWith(['note.md'])
+ expect(asyncVaultRefresh).toHaveBeenCalledWith(['note.md'], '/Users/luca/Laputa')
})
expect(onToast).not.toHaveBeenCalledWith('Pulled 1 update(s) from remote')
@@ -417,7 +417,7 @@ describe('useAutoSync', () => {
})
await waitFor(() => {
- expect(onVaultUpdated).toHaveBeenCalledWith(['note.md'])
+ expect(onVaultUpdated).toHaveBeenCalledWith(['note.md'], '/Users/luca/Laputa')
expect(onSyncUpdated).toHaveBeenCalled()
expect(onToast).toHaveBeenCalledWith('Pulled and pushed successfully')
expect(result.current.syncStatus).toBe('idle')
@@ -459,6 +459,29 @@ describe('useAutoSync', () => {
})
})
+ it('manual triggerSync targets an explicit repository path', async () => {
+ const { result } = renderSync()
+ await waitFor(() => {
+ expect(result.current.syncStatus).toBe('idle')
+ })
+
+ mockInvokeFn.mockClear()
+ mockInvokeFn.mockImplementation((cmd: string) => {
+ if (cmd === 'get_last_commit_info') return Promise.resolve(MOCK_COMMIT_INFO)
+ if (cmd === 'git_remote_status') return Promise.resolve({ branch: 'main', ahead: 0, behind: 0, hasRemote: true })
+ return Promise.resolve(updated(['work.md']))
+ })
+
+ await act(async () => {
+ result.current.triggerSync('/Users/luca/Work')
+ })
+
+ await waitFor(() => {
+ expect(mockInvokeFn).toHaveBeenCalledWith('git_pull', { vaultPath: '/Users/luca/Work' })
+ expect(onVaultUpdated).toHaveBeenCalledWith(['work.md'], '/Users/luca/Work')
+ })
+ })
+
it('surfaces pull conflicts and pull errors during recovery pushes', async () => {
let mode: 'conflict' | 'error' = 'conflict'
diff --git a/src/hooks/useAutoSync.ts b/src/hooks/useAutoSync.ts
index a5257132..deb623e0 100644
--- a/src/hooks/useAutoSync.ts
+++ b/src/hooks/useAutoSync.ts
@@ -20,7 +20,7 @@ interface UseAutoSyncOptions {
enabled?: boolean
vaultPath: string
intervalMinutes: number | null
- onVaultUpdated: (updatedFiles: string[]) => MaybePromise
+ onVaultUpdated: (updatedFiles: string[], vaultPath: string) => MaybePromise
onSyncUpdated?: () => MaybePromise
onConflict: (files: string[]) => void
onToast: (msg: string) => void
@@ -32,9 +32,9 @@ export interface AutoSyncState {
conflictFiles: string[]
lastCommitInfo: LastCommitInfo | null
remoteStatus: GitRemoteStatus | null
- triggerSync: () => void
+ triggerSync: (vaultPath?: string) => void
/** Pull from remote, then push if there are local commits ahead. */
- pullAndPush: () => void
+ pullAndPush: (vaultPath?: string) => void
/** Pause auto-pull (e.g. while conflict resolver modal is open). */
pausePull: () => void
/** Resume auto-pull after pausing. */
@@ -82,19 +82,20 @@ function setConflictState(
function markPullTimestamp(
setLastSyncTime: SyncSetState,
- refreshCommitInfo: () => void,
+ refreshCommitInfo: (vaultPath?: string) => void,
+ vaultPath?: string,
): void {
setLastSyncTime(Date.now())
- refreshCommitInfo()
+ refreshCommitInfo(vaultPath)
}
function useRemoteStatusRefresher(
vaultPath: string,
setRemoteStatus: SyncSetState,
) {
- return useCallback(async () => {
+ return useCallback(async (targetVaultPath = vaultPath) => {
try {
- const status = await tauriCall('git_remote_status', { vaultPath })
+ const status = await tauriCall('git_remote_status', { vaultPath: targetVaultPath })
setRemoteStatus(status)
return status
} catch {
@@ -109,9 +110,9 @@ function useConflictChecker(
setConflictFiles: SyncSetState,
callbacksRef: MutableRefObject,
) {
- return useCallback(async (): Promise => {
+ return useCallback(async (targetVaultPath = vaultPath): Promise => {
try {
- const files = await tauriCall('get_conflict_files', { vaultPath })
+ const files = await tauriCall('get_conflict_files', { vaultPath: targetVaultPath })
if (!Array.isArray(files) || files.length === 0) return false
setConflictState(files, setSyncStatus, setConflictFiles, callbacksRef)
return true
@@ -125,8 +126,8 @@ function useCommitInfoRefresher(
vaultPath: string,
setLastCommitInfo: SyncSetState,
) {
- return useCallback(() => {
- tauriCall('get_last_commit_info', { vaultPath })
+ return useCallback((targetVaultPath = vaultPath) => {
+ tauriCall('get_last_commit_info', { vaultPath: targetVaultPath })
.then(info => setLastCommitInfo(info))
.catch((err) => console.warn('[sync] Failed to refresh last commit info:', err))
}, [vaultPath, setLastCommitInfo])
@@ -134,18 +135,20 @@ function useCommitInfoRefresher(
async function handleUpdatedPull(options: {
result: GitPullResult
+ vaultPath: string
callbacksRef: MutableRefObject
setConflictFiles: SyncSetState
setSyncStatus: SyncSetState
}): Promise {
const {
result,
+ vaultPath,
callbacksRef,
setConflictFiles,
setSyncStatus,
} = options
clearConflictState(setSyncStatus, setConflictFiles)
- await callbacksRef.current.onVaultUpdated(result.updatedFiles)
+ await callbacksRef.current.onVaultUpdated(result.updatedFiles, vaultPath)
await callbacksRef.current.onSyncUpdated?.()
await callbacksRef.current.onToast(`Pulled ${result.updatedFiles.length} update(s) from remote`)
}
@@ -284,7 +287,7 @@ export function useAutoSync({
const checkExistingConflicts = useConflictChecker(vaultPath, setSyncStatus, setConflictFiles, callbacksRef)
const refreshCommitInfo = useCommitInfoRefresher(vaultPath, setLastCommitInfo)
- const performPull = useCallback(async () => {
+ const performPull = useCallback(async (targetVaultPath = vaultPath) => {
if (!enabled) return
await runSyncTask({
@@ -294,12 +297,13 @@ export function useAutoSync({
setLastSyncTime,
setSyncStatus,
task: async () => {
- const result = await tauriCall('git_pull', { vaultPath })
- markPullTimestamp(setLastSyncTime, refreshCommitInfo)
+ const result = await tauriCall('git_pull', { vaultPath: targetVaultPath })
+ markPullTimestamp(setLastSyncTime, refreshCommitInfo, targetVaultPath)
if (result.status === 'updated') {
await handleUpdatedPull({
result,
+ vaultPath: targetVaultPath,
callbacksRef,
setConflictFiles,
setSyncStatus,
@@ -308,7 +312,7 @@ export function useAutoSync({
setConflictState(result.conflictFiles, setSyncStatus, setConflictFiles, callbacksRef)
} else if (result.status === 'error') {
await resolvePullError({
- checkExistingConflicts,
+ checkExistingConflicts: () => checkExistingConflicts(targetVaultPath),
callbacksRef,
setSyncStatus,
})
@@ -316,13 +320,13 @@ export function useAutoSync({
clearConflictState(setSyncStatus, setConflictFiles)
}
- void refreshRemoteStatus()
+ void refreshRemoteStatus(targetVaultPath)
},
})
}, [enabled, vaultPath, refreshCommitInfo, checkExistingConflicts, refreshRemoteStatus])
/** Pull from remote, then auto-push if successful. Used for divergence recovery. */
- const pullAndPush = useCallback(async () => {
+ const pullAndPush = useCallback(async (targetVaultPath = vaultPath) => {
if (!enabled) return
await runSyncTask({
@@ -332,8 +336,8 @@ export function useAutoSync({
setLastSyncTime,
setSyncStatus,
task: async () => {
- const pullResult = await tauriCall('git_pull', { vaultPath })
- markPullTimestamp(setLastSyncTime, refreshCommitInfo)
+ const pullResult = await tauriCall('git_pull', { vaultPath: targetVaultPath })
+ markPullTimestamp(setLastSyncTime, refreshCommitInfo, targetVaultPath)
if (pullResult.status === 'conflict') {
setConflictState(pullResult.conflictFiles, setSyncStatus, setConflictFiles, callbacksRef)
@@ -342,7 +346,7 @@ export function useAutoSync({
if (pullResult.status === 'error') {
await resolvePullError({
- checkExistingConflicts,
+ checkExistingConflicts: () => checkExistingConflicts(targetVaultPath),
notifyError: `Pull failed: ${pullResult.message}`,
callbacksRef,
setSyncStatus,
@@ -351,11 +355,11 @@ export function useAutoSync({
}
if (pullResult.status === 'updated') {
- await callbacksRef.current.onVaultUpdated(pullResult.updatedFiles)
+ await callbacksRef.current.onVaultUpdated(pullResult.updatedFiles, targetVaultPath)
await callbacksRef.current.onSyncUpdated?.()
}
- const pushResult = await tauriCall('git_push', { vaultPath })
+ const pushResult = await tauriCall('git_push', { vaultPath: targetVaultPath })
handlePushResult({
pushResult,
callbacksRef,
@@ -363,7 +367,7 @@ export function useAutoSync({
setSyncStatus,
})
- void refreshRemoteStatus()
+ void refreshRemoteStatus(targetVaultPath)
},
})
}, [enabled, vaultPath, refreshCommitInfo, checkExistingConflicts, refreshRemoteStatus])
@@ -383,12 +387,12 @@ export function useAutoSync({
const pausePull = useCallback(() => { pauseRef.current = true }, [])
const resumePull = useCallback(() => { pauseRef.current = false }, [])
- const triggerSync = useCallback(() => {
+ const triggerSync = useCallback((targetVaultPath = vaultPath) => {
if (!enabled) return
trackEvent('sync_triggered')
- void performPull()
- }, [enabled, performPull])
+ void performPull(targetVaultPath)
+ }, [enabled, performPull, vaultPath])
return { syncStatus, lastSyncTime, conflictFiles, lastCommitInfo, remoteStatus, triggerSync, pullAndPush, pausePull, resumePull, handlePushRejected }
}
diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts
index c1e65539..9901e6b1 100644
--- a/src/hooks/useCommandRegistry.test.ts
+++ b/src/hooks/useCommandRegistry.test.ts
@@ -151,6 +151,26 @@ describe('useCommandRegistry', () => {
expect(findCommand(result.current, 'view-changes')).toBeUndefined()
})
+ it('exposes one pull command per active repository when multiple Git targets are available', () => {
+ const onPullRepository = vi.fn()
+ const config = makeConfig({
+ gitRepositories: [
+ { path: '/vault/main', label: 'Main Vault', defaultForNewNotes: true },
+ { path: '/vault/brian', label: 'Brian', defaultForNewNotes: false },
+ ],
+ onPullRepository,
+ })
+ const { result } = renderHook(() => useCommandRegistry(config))
+
+ const mainPull = findCommand(result.current, 'git-pull-0')
+ const brianPull = findCommand(result.current, 'git-pull-1')
+ expect(mainPull?.label).toBe('Pull from Remote: Main Vault')
+ expect(brianPull?.label).toBe('Pull from Remote: Brian')
+
+ brianPull?.execute()
+ expect(onPullRepository).toHaveBeenCalledWith('/vault/brian')
+ })
+
it('resolve-conflicts stays enabled across rerenders', () => {
const config = makeConfig()
const { result, rerender } = renderHook(
diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts
index f3c1958a..bd971b32 100644
--- a/src/hooks/useCommandRegistry.ts
+++ b/src/hooks/useCommandRegistry.ts
@@ -16,6 +16,7 @@ import { buildTypeCommands } from './commands/typeCommands'
import { buildFilterCommands } from './commands/filterCommands'
import { localizeCommandActions } from './commands/localizeCommands'
import { extractVaultTypes } from '../utils/vaultTypes'
+import type { GitRepositoryOption } from '../utils/gitRepositories'
// Re-export types and helpers for backward compatibility
export type { CommandAction, CommandGroup } from './commands/types'
@@ -76,6 +77,7 @@ interface CommandRegistryConfig {
canAddRemote?: boolean
gitFeaturesEnabled?: boolean
isGitVault?: boolean
+ gitRepositories?: GitRepositoryOption[]
onInitializeGit?: () => void
onCreateType?: () => void
onDeleteNote: (path: string) => void
@@ -83,6 +85,7 @@ interface CommandRegistryConfig {
onUnarchiveNote: (path: string) => void
onCommitPush: () => void
onPull?: () => void
+ onPullRepository?: (path: string) => void
onResolveConflicts?: () => void
onSetViewMode: (mode: ViewMode) => void
onToggleInspector: () => void
@@ -151,7 +154,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
onCustomizeNoteListColumns, canCustomizeNoteListColumns,
onRestoreDeletedNote, canRestoreDeletedNote,
selection, noteListFilter, onSetNoteListFilter,
- gitFeaturesEnabled, isGitVault, onInitializeGit,
+ gitFeaturesEnabled, isGitVault, gitRepositories, onInitializeGit, onPullRepository,
} = config
const hasActiveNote = activeTabPath !== null
@@ -215,16 +218,18 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com
modifiedCount,
gitFeaturesEnabled,
isGitVault,
+ repositories: gitRepositories,
canAddRemote: config.canAddRemote ?? false,
onAddRemote: config.onAddRemote,
onCommitPush,
onInitializeGit,
onPull,
+ onPullRepository,
onResolveConflicts,
onSelect,
}), [
- modifiedCount, gitFeaturesEnabled, isGitVault, config.canAddRemote, config.onAddRemote,
- onCommitPush, onInitializeGit, onPull, onResolveConflicts, onSelect,
+ modifiedCount, gitFeaturesEnabled, isGitVault, gitRepositories, config.canAddRemote, config.onAddRemote,
+ onCommitPush, onInitializeGit, onPull, onPullRepository, onResolveConflicts, onSelect,
])
const viewCommands = useMemo(() => buildViewCommands({
diff --git a/src/hooks/useGitRepositories.test.ts b/src/hooks/useGitRepositories.test.ts
index 7d19eb0e..95e4fb6d 100644
--- a/src/hooks/useGitRepositories.test.ts
+++ b/src/hooks/useGitRepositories.test.ts
@@ -118,4 +118,27 @@ describe('useGitRepositories', () => {
expect(mockInvoke).toHaveBeenCalledWith('get_modified_files', { vaultPath: '/default', includeStats: true })
})
+
+ it('keeps an explicit sync repository selection separate from other Git surfaces', async () => {
+ const repositories = [
+ { path: '/default', label: 'Default', defaultForNewNotes: true },
+ { path: '/work', label: 'Work', defaultForNewNotes: false },
+ ]
+ vi.mocked(mockInvoke).mockResolvedValue([])
+
+ const { result } = renderHook(() => useGitRepositories({
+ defaultVaultPath: '/default',
+ repositories,
+ }))
+
+ await waitFor(() => expect(mockInvoke).toHaveBeenCalledWith('get_modified_files', { vaultPath: '/default', includeStats: false }))
+
+ act(() => {
+ result.current.setSyncRepositoryPath('/work')
+ })
+
+ expect(result.current.syncRepositoryPath).toBe('/work')
+ expect(result.current.commitRepositoryPath).toBe('/default')
+ expect(result.current.changesRepositoryPath).toBe('/default')
+ })
})
diff --git a/src/hooks/useGitRepositories.ts b/src/hooks/useGitRepositories.ts
index d67cbd2d..614ab3ac 100644
--- a/src/hooks/useGitRepositories.ts
+++ b/src/hooks/useGitRepositories.ts
@@ -181,6 +181,7 @@ export function useGitRepositories({
const [changesRepositoryPath, setChangesRepositoryPath] = useValidatedRepositoryPath(selectionConfig)
const [historyRepositoryPath, setHistoryRepositoryPath] = useValidatedRepositoryPath(selectionConfig)
const [commitRepositoryPath, setCommitRepositoryPath] = useValidatedRepositoryPath(selectionConfig)
+ const [syncRepositoryPath, setSyncRepositoryPath] = useValidatedRepositoryPath(selectionConfig)
const { byRepository, loadAllModifiedFiles, loadModifiedFilesForRepository } = useRepositoryModifiedFiles(repositories)
const {
byRepository: remoteStatusByRepository,
@@ -234,6 +235,8 @@ export function useGitRepositories({
setChangesRepositoryPath,
setCommitRepositoryPath,
setHistoryRepositoryPath,
+ setSyncRepositoryPath,
+ syncRepositoryPath,
totalModifiedCount: allModifiedFiles.length,
}
}
diff --git a/src/lib/locales/de-DE.json b/src/lib/locales/de-DE.json
index b7c58f1e..fccb8def 100644
--- a/src/lib/locales/de-DE.json
+++ b/src/lib/locales/de-DE.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Commit & Push",
"command.git.addRemote": "Remote zum aktuellen Vault hinzufügen",
"command.git.pull": "Von Remote abrufen",
+ "command.git.pullRepository": "Aus Remote abrufen: {repository}",
"command.git.resolveConflicts": "Konflikte auflösen",
"command.git.viewChanges": "Ausstehende Änderungen anzeigen",
"git.repository.select": "Repository",
diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json
index bf9986b6..cfec816f 100644
--- a/src/lib/locales/en.json
+++ b/src/lib/locales/en.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Commit & Push",
"command.git.addRemote": "Add Remote to Current Vault",
"command.git.pull": "Pull from Remote",
+ "command.git.pullRepository": "Pull from Remote: {repository}",
"command.git.resolveConflicts": "Resolve Conflicts",
"command.git.viewChanges": "View Pending Changes",
"git.repository.select": "Repository",
diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json
index 5bfd19f9..0c48188f 100644
--- a/src/lib/locales/es-419.json
+++ b/src/lib/locales/es-419.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Confirmar y enviar",
"command.git.addRemote": "Agregar repositorio remoto al repositorio actual",
"command.git.pull": "Extraer desde el repositorio remoto",
+ "command.git.pullRepository": "Extraer del repositorio remoto: {repository}",
"command.git.resolveConflicts": "Resolver conflictos",
"command.git.viewChanges": "Ver cambios pendientes",
"git.repository.select": "Repositorio",
diff --git a/src/lib/locales/es-ES.json b/src/lib/locales/es-ES.json
index 54eca87d..1c981957 100644
--- a/src/lib/locales/es-ES.json
+++ b/src/lib/locales/es-ES.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Confirmar y enviar",
"command.git.addRemote": "Añadir repositorio remoto al almacén actual",
"command.git.pull": "Extraer del repositorio remoto",
+ "command.git.pullRepository": "Extraer del repositorio remoto: {repository}",
"command.git.resolveConflicts": "Resolver conflictos",
"command.git.viewChanges": "Ver cambios pendientes",
"git.repository.select": "Repositorio",
diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json
index 4ed3991a..badf307f 100644
--- a/src/lib/locales/fr-FR.json
+++ b/src/lib/locales/fr-FR.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Valider et pousser",
"command.git.addRemote": "Ajouter le dépôt distant au coffre-fort actuel",
"command.git.pull": "Extraire depuis le dépôt distant",
+ "command.git.pullRepository": "Extraire du dépôt distant : {repository}",
"command.git.resolveConflicts": "Résoudre les conflits",
"command.git.viewChanges": "Afficher les modifications en attente",
"git.repository.select": "Dépôt",
diff --git a/src/lib/locales/it-IT.json b/src/lib/locales/it-IT.json
index a9823e11..07f6e879 100644
--- a/src/lib/locales/it-IT.json
+++ b/src/lib/locales/it-IT.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Commit e push",
"command.git.addRemote": "Aggiungi repository remoto al vault corrente",
"command.git.pull": "Esegui il pull dal repository remoto",
+ "command.git.pullRepository": "Esegui il pull dal repository remoto: {repository}",
"command.git.resolveConflicts": "Risolvi conflitti",
"command.git.viewChanges": "Visualizza modifiche in sospeso",
"git.repository.select": "Repository",
diff --git a/src/lib/locales/ja-JP.json b/src/lib/locales/ja-JP.json
index 80d4d668..6426a153 100644
--- a/src/lib/locales/ja-JP.json
+++ b/src/lib/locales/ja-JP.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "コミット & プッシュ",
"command.git.addRemote": "現在のボールトにリモートを追加",
"command.git.pull": "リモートからプル",
+ "command.git.pullRepository": "リモートからプル:{repository}",
"command.git.resolveConflicts": "競合を解決",
"command.git.viewChanges": "保留中の変更を表示",
"git.repository.select": "リポジトリ",
diff --git a/src/lib/locales/ko-KR.json b/src/lib/locales/ko-KR.json
index 3d0ae42e..473059a6 100644
--- a/src/lib/locales/ko-KR.json
+++ b/src/lib/locales/ko-KR.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "커밋 및 푸시",
"command.git.addRemote": "현재 Vault에 원격 저장소 추가",
"command.git.pull": "원격에서 가져오기",
+ "command.git.pullRepository": "원격에서 가져오기: {repository}",
"command.git.resolveConflicts": "충돌 해결",
"command.git.viewChanges": "보류 중인 변경 사항 보기",
"git.repository.select": "저장소",
diff --git a/src/lib/locales/pl-PL.json b/src/lib/locales/pl-PL.json
index 9291061a..21353127 100644
--- a/src/lib/locales/pl-PL.json
+++ b/src/lib/locales/pl-PL.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Zatwierdź i wypchnij",
"command.git.addRemote": "Dodaj zdalne repozytorium do bieżącego sejfu",
"command.git.pull": "Pobierz ze zdalnego repozytorium",
+ "command.git.pullRepository": "Pobierz z repozytorium zdalnego: {repository}",
"command.git.resolveConflicts": "Rozwiąż konflikty",
"command.git.viewChanges": "Pokaż oczekujące zmiany",
"git.repository.select": "Repozytorium",
diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json
index 65154c86..4e90fc28 100644
--- a/src/lib/locales/pt-BR.json
+++ b/src/lib/locales/pt-BR.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Fazer commit e push",
"command.git.addRemote": "Adicionar repositório remoto ao cofre atual",
"command.git.pull": "Fazer pull do repositório remoto",
+ "command.git.pullRepository": "Puxar do Remoto: {repository}",
"command.git.resolveConflicts": "Resolver conflitos",
"command.git.viewChanges": "Exibir alterações pendentes",
"git.repository.select": "Repositório",
diff --git a/src/lib/locales/pt-PT.json b/src/lib/locales/pt-PT.json
index 101a79db..14427bfd 100644
--- a/src/lib/locales/pt-PT.json
+++ b/src/lib/locales/pt-PT.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Fazer commit e push",
"command.git.addRemote": "Adicionar repositório remoto ao cofre atual",
"command.git.pull": "Efetuar pull do repositório remoto",
+ "command.git.pullRepository": "Obter do Remoto: {repository}",
"command.git.resolveConflicts": "Resolver conflitos",
"command.git.viewChanges": "Ver alterações pendentes",
"git.repository.select": "Repositório",
diff --git a/src/lib/locales/ru-RU.json b/src/lib/locales/ru-RU.json
index dc8705bb..a4fe2dad 100644
--- a/src/lib/locales/ru-RU.json
+++ b/src/lib/locales/ru-RU.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Commit и Push",
"command.git.addRemote": "Добавить удаленный репозиторий в текущий хранилище",
"command.git.pull": "Pull from Remote",
+ "command.git.pullRepository": "Получить из удаленного репозитория: {repository}",
"command.git.resolveConflicts": "Разрешить конфликты",
"command.git.viewChanges": "Просмотреть ожидающие изменения",
"git.repository.select": "Репозиторий",
diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json
index 0e6c90a4..6ef7d21c 100644
--- a/src/lib/locales/vi.json
+++ b/src/lib/locales/vi.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "Commit & Push",
"command.git.addRemote": "Thêm remote cho kho hiện tại",
"command.git.pull": "Kéo từ remote",
+ "command.git.pullRepository": "Lấy từ Remote: {repository}",
"command.git.resolveConflicts": "Giải quyết xung đột",
"command.git.viewChanges": "Xem các thay đổi đang chờ",
"git.repository.select": "Kho lưu trữ",
diff --git a/src/lib/locales/zh-CN.json b/src/lib/locales/zh-CN.json
index 816c644a..1d3d3ad9 100644
--- a/src/lib/locales/zh-CN.json
+++ b/src/lib/locales/zh-CN.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "提交并推送",
"command.git.addRemote": "为当前仓库添加远程地址",
"command.git.pull": "从远程拉取",
+ "command.git.pullRepository": "从远程拉取:{repository}",
"command.git.resolveConflicts": "解决冲突",
"command.git.viewChanges": "查看待处理更改",
"git.repository.select": "仓库",
diff --git a/src/lib/locales/zh-TW.json b/src/lib/locales/zh-TW.json
index 7759bbc0..d95894be 100644
--- a/src/lib/locales/zh-TW.json
+++ b/src/lib/locales/zh-TW.json
@@ -57,6 +57,7 @@
"command.git.commitPush": "提交併推送",
"command.git.addRemote": "為當前倉庫新增遠端地址",
"command.git.pull": "從遠端拉取",
+ "command.git.pullRepository": "從遠端提取:{repository}",
"command.git.resolveConflicts": "解決衝突",
"command.git.viewChanges": "檢視待處理更改",
"git.repository.select": "儲存庫",