diff --git a/lara.lock b/lara.lock index a4a04043..40080338 100644 --- a/lara.lock +++ b/lara.lock @@ -59,6 +59,7 @@ files: command.git.commitPush: 8cb5faa4019716f43dd69c41496b1d46 command.git.addRemote: 7eef951b3f909340a68dc9811be83e9e command.git.pull: 3988d61f4a4546381f61a4eaf61315b4 + command.git.pullRepository: 931ad25176586fd4332dd671574d3185 command.git.resolveConflicts: 871ac36968cfca3d2297a89b28b25dee command.git.viewChanges: b2699edf69d8e1031afce2b2f94e5c8d git.repository.select: 33fcf2b3ec4686d9cd06051c726d0ba2 diff --git a/src/App.tsx b/src/App.tsx index 8496592b..3a243b6e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -636,8 +636,9 @@ function App() { }) const handleVaultUpdate = useCallback(async ( updatedFiles: string[], - options: { preserveFocusedEditor?: boolean } = {}, + options: { preserveFocusedEditor?: boolean; vaultPath?: string } = {}, ) => { + const updateVaultPath = options.vaultPath ?? resolvedPath await refreshPulledVaultState({ activeTabPath: notes.activeTabPath, closeAllTabs, @@ -651,7 +652,7 @@ function App() { reloadViews: vault.reloadViews, replaceActiveTab: handleReplaceActiveTab, updatedFiles, - vaultPath: resolvedPath, + vaultPath: updateVaultPath, }) await refreshGitModifiedFiles() }, [ @@ -667,7 +668,10 @@ function App() { vault.unsavedPaths, ]) const handlePulledVaultUpdate = useCallback( - (updatedFiles: string[]) => handleVaultUpdate(updatedFiles, getPulledVaultUpdateOptions()), + (updatedFiles: string[], vaultPath: string) => handleVaultUpdate(updatedFiles, { + ...getPulledVaultUpdateOptions(), + vaultPath, + }), [handleVaultUpdate], ) const handleFocusedVaultUpdate = useCallback( @@ -694,7 +698,7 @@ function App() { }) const autoSync = useAutoSync({ enabled: gitFeaturesEnabled && gitRepoState === 'ready', - vaultPath: resolvedPath, + vaultPath: gitSurfaces.syncRepositoryPath, intervalMinutes: settings.auto_pull_interval_minutes, onVaultUpdated: handlePulledVaultUpdate, onConflict: (files) => { @@ -747,7 +751,8 @@ function App() { }) const conflictFlow = useConflictFlow({ - resolvedPath, entries: visibleEntries, + resolvedPath: gitSurfaces.syncRepositoryPath, + entries: visibleEntries, conflictFiles: autoSync.conflictFiles, pausePull: autoSync.pausePull, resumePull: autoSync.resumePull, triggerSync: autoSync.triggerSync, reloadVault: vault.reloadVault, @@ -827,7 +832,7 @@ function App() { onSetFilter: (filterType) => { handleSetSelection({ kind: 'sectionGroup', type: filterType }) }, - onVaultChanged: (path) => { void handlePulledVaultUpdate(path ? [path] : []) }, + onVaultChanged: (path) => { void handlePulledVaultUpdate(path ? [path] : [], resolvedPath) }, }) const handleInitializeProperties = useCallback(async (path: string) => { @@ -1014,6 +1019,10 @@ function App() { const handleAppContentChange = appSave.handleContentChange const handleAppSave = appSave.handleSave const loadModifiedFiles = refreshGitModifiedFiles + const setSyncRepositoryPath = gitSurfaces.setSyncRepositoryPath + const syncRepositoryPath = gitSurfaces.syncRepositoryPath + const triggerSync = autoSync.triggerSync + const pullAndPush = autoSync.pullAndPush useEffect(() => { if (!gitFeaturesEnabled) return @@ -1034,6 +1043,17 @@ function App() { runAutomaticCheckpoint, }) }, [gitFeaturesEnabled, openCommitDialog, runAutomaticCheckpoint, settings.autogit_enabled]) + const handlePullRepository = useCallback((targetVaultPath: string) => { + if (!gitFeaturesEnabled) return + setSyncRepositoryPath(targetVaultPath) + triggerSync(targetVaultPath) + }, [gitFeaturesEnabled, setSyncRepositoryPath, triggerSync]) + const handlePullSelectedRepository = useCallback(() => { + handlePullRepository(syncRepositoryPath) + }, [handlePullRepository, syncRepositoryPath]) + const handlePullAndPushSelectedRepository = useCallback(() => { + pullAndPush(syncRepositoryPath) + }, [pullAndPush, syncRepositoryPath]) const handleTrackedContentChange = useCallback((path: string, content: string) => { recordAutoGitActivity() @@ -1553,10 +1573,12 @@ function App() { onDeleteNote: deleteActions.handleDeleteNote, onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote, onCommitPush: handleCommitPush, + gitRepositories, gitFeaturesEnabled, isGitVault, onInitializeGit: openGitSetupDialog, - onPull: autoSync.triggerSync, + onPull: handlePullSelectedRepository, + onPullRepository: handlePullRepository, onResolveConflicts: conflictFlow.handleOpenConflictResolver, onSetViewMode: handleSetViewMode, onToggleInspector: handleToggleInspector, @@ -1786,7 +1808,7 @@ function App() { - handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} gitFeaturesEnabled={gitFeaturesEnabled} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} aiFeaturesEnabled={aiFeaturesEnabled} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiFeaturesEnabled ? aiAgentsStatus : undefined} vaultAiGuidanceStatus={aiFeaturesEnabled ? vaultAiGuidanceStatus : undefined} defaultAiAgent={aiFeaturesEnabled ? aiAgentPreferences.defaultAiAgent : undefined} defaultAiTarget={aiFeaturesEnabled ? settings.default_ai_target ?? undefined : undefined} aiModelProviders={aiFeaturesEnabled ? settings.ai_model_providers ?? [] : []} onSetDefaultAiAgent={aiFeaturesEnabled ? aiAgentPreferences.setDefaultAiAgent : undefined} onSetDefaultAiTarget={aiFeaturesEnabled ? aiAgentPreferences.setDefaultAiTarget : undefined} onRestoreVaultAiGuidance={aiFeaturesEnabled ? () => { void restoreVaultAiGuidance() } : undefined} locale={appLocale} /> + handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} gitFeaturesEnabled={gitFeaturesEnabled} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.syncRepositoryPath} onRepositoryChange={gitSurfaces.setSyncRepositoryPath} onTriggerSync={handlePullSelectedRepository} onPullAndPush={handlePullAndPushSelectedRepository} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} aiFeaturesEnabled={aiFeaturesEnabled} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiFeaturesEnabled ? aiAgentsStatus : undefined} vaultAiGuidanceStatus={aiFeaturesEnabled ? vaultAiGuidanceStatus : undefined} defaultAiAgent={aiFeaturesEnabled ? aiAgentPreferences.defaultAiAgent : undefined} defaultAiTarget={aiFeaturesEnabled ? settings.default_ai_target ?? undefined : undefined} aiModelProviders={aiFeaturesEnabled ? settings.ai_model_providers ?? [] : []} onSetDefaultAiAgent={aiFeaturesEnabled ? aiAgentPreferences.setDefaultAiAgent : undefined} onSetDefaultAiTarget={aiFeaturesEnabled ? aiAgentPreferences.setDefaultAiTarget : undefined} onRestoreVaultAiGuidance={aiFeaturesEnabled ? () => { void restoreVaultAiGuidance() } : undefined} locale={appLocale} /> setToastMessage(null)} /> diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx index ef3dbc42..a24691d5 100644 --- a/src/components/Editor.test.tsx +++ b/src/components/Editor.test.tsx @@ -573,7 +573,7 @@ describe('Editor', () => { fireEvent.click(screen.getByRole('button', { name: 'Open table of contents' })) expect(screen.getByTestId('table-of-contents-panel')).toBeInTheDocument() - expect(await screen.findByRole('button', { name: 'Table Heading' })).toBeInTheDocument() + expect(await screen.findByRole('button', { name: 'Table Heading' }, { timeout: 5000 })).toBeInTheDocument() }) // Regression: editor content did not appear on first load because BlockNote's diff --git a/src/components/StatusBar.test.tsx b/src/components/StatusBar.test.tsx index 93b40058..50020f51 100644 --- a/src/components/StatusBar.test.tsx +++ b/src/components/StatusBar.test.tsx @@ -749,6 +749,30 @@ describe('StatusBar', () => { expect(screen.getByText(/1 behind/)).toBeInTheDocument() }) + it('shows the selected repository on sync controls when multiple vaults are active', () => { + render( + + ) + + expect(screen.getByTestId('status-sync')).toHaveTextContent('Work Vault') + fireEvent.click(screen.getByTestId('status-sync')) + expect(screen.getByTestId('git-status-repository-select')).toBeInTheDocument() + }) + it('shows History badge in status bar', () => { render() expect(screen.getByTestId('status-pulse')).toBeInTheDocument() diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index bc2174a2..8fcc76d0 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -8,6 +8,7 @@ import type { ThemeMode } from '../lib/themeMode' import type { AppLocale } from '../lib/i18n' import type { GitRemoteStatus, SyncStatus } from '../types' import { TooltipProvider } from '@/components/ui/tooltip' +import type { GitRepositoryOption } from '../utils/gitRepositories' import { StatusBarPrimarySection, StatusBarSecondarySection, @@ -84,6 +85,9 @@ interface StatusBarProps { lastSyncTime?: number | null conflictCount?: number remoteStatus?: GitRemoteStatus | null + repositories?: GitRepositoryOption[] + selectedRepositoryPath?: string + onRepositoryChange?: (path: string) => void onTriggerSync?: () => void onPullAndPush?: () => void onOpenConflictResolver?: () => void @@ -145,6 +149,9 @@ function StatusBarPrimaryFromFooter({ lastSyncTime = null, conflictCount = 0, remoteStatus, + repositories, + selectedRepositoryPath, + onRepositoryChange, onTriggerSync, onPullAndPush, onOpenConflictResolver, @@ -197,6 +204,9 @@ function StatusBarPrimaryFromFooter({ lastSyncTime={lastSyncTime} conflictCount={conflictCount} remoteStatus={remoteStatus} + repositories={repositories} + selectedRepositoryPath={selectedRepositoryPath} + onRepositoryChange={onRepositoryChange} onTriggerSync={onTriggerSync} onPullAndPush={onPullAndPush} onOpenConflictResolver={onOpenConflictResolver} diff --git a/src/components/codeBlockOptions.ts b/src/components/codeBlockOptions.ts index 271b6f82..9b6510bf 100644 --- a/src/components/codeBlockOptions.ts +++ b/src/components/codeBlockOptions.ts @@ -4,8 +4,50 @@ import { supportsModernRegexFeatures } from '../utils/regexCapabilities' const LIGHT_CODE_THEME = 'github-light' const DARK_CODE_THEME = 'github-dark' +const GO_LANGUAGE = { name: 'Go', aliases: ['go', 'golang'] } +const GO_LANGUAGE_REGISTRATION = { + name: 'go', + displayName: 'Go', + scopeName: 'source.go', + aliases: ['golang'], + patterns: [ + { include: '#comments' }, + { include: '#strings' }, + { include: '#keywords' }, + { include: '#numbers' }, + ], + repository: { + comments: { + patterns: [ + { begin: '/\\*', end: '\\*/', name: 'comment.block.go' }, + { begin: '//', end: '$', name: 'comment.line.double-slash.go' }, + ], + }, + keywords: { + patterns: [ + { + match: '\\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\\b', + name: 'keyword.control.go', + }, + ], + }, + numbers: { + patterns: [ + { match: '\\b0[xX][0-9a-fA-F_]+\\b|\\b\\d[\\d_]*(\\.\\d[\\d_]*)?\\b', name: 'constant.numeric.go' }, + ], + }, + strings: { + patterns: [ + { begin: '"', end: '"', name: 'string.quoted.double.go' }, + { begin: '`', end: '`', name: 'string.quoted.raw.go' }, + ], + }, + }, +} type TolariaCodeHighlighter = Awaited>> +type TolariaLoadLanguage = TolariaCodeHighlighter['loadLanguage'] +type TolariaLanguageInput = Parameters[number] function currentCodeBlockTheme() { if (typeof document === 'undefined') return LIGHT_CODE_THEME @@ -20,11 +62,20 @@ function prioritizeTheme(themes: string[], theme: string) { return [theme, ...themes.filter((candidate) => candidate !== theme)] } +function expandGoLanguage(language: TolariaLanguageInput): TolariaLanguageInput[] { + if (typeof language !== 'string') return [language] + const languageName: string = language + return languageName === 'go' || languageName === 'golang' + ? [GO_LANGUAGE_REGISTRATION as TolariaLanguageInput] + : [language] +} + async function createTolariaCodeHighlighter(): Promise { const highlighter = await codeBlockOptions.createHighlighter() return { ...highlighter, getLoadedThemes: () => prioritizeTheme(highlighter.getLoadedThemes(), currentCodeBlockTheme()), + loadLanguage: (...languages) => highlighter.loadLanguage(...languages.flatMap(expandGoLanguage)), } } @@ -33,6 +84,10 @@ export function createTolariaCodeBlockOptions(): Partial { ...codeBlockOptions, createHighlighter: createTolariaCodeHighlighter, defaultLanguage: 'text', + supportedLanguages: { + ...codeBlockOptions.supportedLanguages, + go: GO_LANGUAGE, + }, } if (supportsModernRegexFeatures()) return options diff --git a/src/components/status-bar/StatusBarBadges.extra.test.tsx b/src/components/status-bar/StatusBarBadges.extra.test.tsx index 195bac1f..e590ed67 100644 --- a/src/components/status-bar/StatusBarBadges.extra.test.tsx +++ b/src/components/status-bar/StatusBarBadges.extra.test.tsx @@ -99,12 +99,7 @@ describe('StatusBarBadges extra coverage', () => { expect(screen.getByText('↓ 1 behind')).toBeInTheDocument() expect(screen.getByText(/Status: Synced/)).toBeInTheDocument() - const pullButton = screen.getByTestId('git-status-pull-btn') - fireEvent.mouseEnter(pullButton) - expect(pullButton.style.background).toBe('var(--hover)') - fireEvent.mouseLeave(pullButton) - expect(pullButton.style.background).toBe('transparent') - + const pullButton = screen.getByRole('button', { name: 'Pull' }) fireEvent.click(pullButton) expect(onTriggerSync).toHaveBeenCalledTimes(1) expect(screen.queryByTestId('git-status-popup')).not.toBeInTheDocument() diff --git a/src/components/status-bar/StatusBarBadges.tsx b/src/components/status-bar/StatusBarBadges.tsx index 8c6aba6e..138ee969 100644 --- a/src/components/status-bar/StatusBarBadges.tsx +++ b/src/components/status-bar/StatusBarBadges.tsx @@ -8,6 +8,8 @@ import type { McpStatus } from '../../hooks/useMcpStatus' import { translate, type AppLocale, type TranslationKey } from '../../lib/i18n' import type { GitRemoteStatus, LastCommitInfo, SyncStatus } from '../../types' import { openExternalUrl } from '../../utils/url' +import { gitRepositoryLabel, type GitRepositoryOption } from '../../utils/gitRepositories' +import { GitRepositorySelect } from '../GitRepositorySelect' import { useDismissibleLayer } from './useDismissibleLayer' import { ICON_STYLE, SEP_STYLE } from './styles' @@ -43,6 +45,16 @@ function formatSyncLabel(locale: AppLocale, status: SyncStatus, lastSyncTime: nu return labelKey ? translate(locale, labelKey) : formatElapsedSync(locale, lastSyncTime) } +function formatSyncBadgeLabel( + locale: AppLocale, + status: SyncStatus, + lastSyncTime: number | null, + repositoryLabel?: string | null, +): string { + const label = formatSyncLabel(locale, status, lastSyncTime) + return repositoryLabel ? `${repositoryLabel} · ${label}` : label +} + function syncIconColor(status: SyncStatus): string { return SYNC_COLORS.get(status) ?? 'var(--accent-green)' } @@ -415,29 +427,19 @@ function PullAction({ return (
- +
) } @@ -445,12 +447,18 @@ function PullAction({ function GitStatusPopup({ status, remoteStatus, + repositories = [], + selectedRepositoryPath, + onRepositoryChange, locale = 'en', onPull, onClose, }: { status: SyncStatus remoteStatus: GitRemoteStatus | null + repositories?: GitRepositoryOption[] + selectedRepositoryPath?: string + onRepositoryChange?: (path: string) => void locale?: AppLocale onPull?: () => void onClose: () => void @@ -474,6 +482,17 @@ function GitStatusPopup({ color: 'var(--foreground)', }} > + {repositories.length > 1 && selectedRepositoryPath && onRepositoryChange && ( +
+ +
+ )}
{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": "儲存庫",