diff --git a/src/App.tsx b/src/App.tsx
index 78feb6f3..995a2767 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -35,6 +35,7 @@ import { useGitRemoteStatus } from './hooks/useGitRemoteStatus'
import { useViewMode, type ViewMode } from './hooks/useViewMode'
import { useEntryActions } from './hooks/useEntryActions'
import { useAppCommands } from './hooks/useAppCommands'
+import { triggerCommitEntryAction } from './utils/commitEntryAction'
import { generateCommitMessage } from './utils/commitMessage'
import { useDialogs } from './hooks/useDialogs'
import { useVaultSwitcher } from './hooks/useVaultSwitcher'
@@ -660,6 +661,7 @@ function App() {
onCheckpoint: () => commitFlow.runAutomaticCheckpoint(),
})
const recordAutoGitActivity = autoGit.recordActivity
+ const openCommitDialog = commitFlow.openCommitDialog
const runAutomaticCheckpoint = commitFlow.runAutomaticCheckpoint
const handleAppContentChange = appSave.handleContentChange
const handleAppSave = appSave.handleSave
@@ -670,9 +672,13 @@ function App() {
recordAutoGitActivity()
}, [modifiedFilesSignature, recordAutoGitActivity])
- const handleQuickCommitPush = useCallback(() => {
- void runAutomaticCheckpoint({ savePendingBeforeCommit: true })
- }, [runAutomaticCheckpoint])
+ const handleCommitPush = useCallback(() => {
+ triggerCommitEntryAction({
+ autoGitEnabled: settings.autogit_enabled === true,
+ openCommitDialog,
+ runAutomaticCheckpoint,
+ })
+ }, [openCommitDialog, runAutomaticCheckpoint, settings.autogit_enabled])
const handleTrackedContentChange = useCallback((path: string, content: string) => {
recordAutoGitActivity()
@@ -953,7 +959,7 @@ function App() {
onOpenFeedback: openFeedback,
onDeleteNote: deleteActions.handleDeleteNote,
onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote,
- onCommitPush: commitFlow.openCommitDialog,
+ onCommitPush: handleCommitPush,
onPull: autoSync.triggerSync,
onResolveConflicts: conflictFlow.handleOpenConflictResolver,
onSetViewMode: handleSetViewMode,
@@ -1174,7 +1180,7 @@ function App() {
- handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleQuickCommitPush} isOffline={networkStatus.isOffline} isGitVault={isGitVault} 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} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={installMcp} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} />
+ handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} isOffline={networkStatus.isOffline} isGitVault={isGitVault} 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} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={installMcp} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} />
setToastMessage(null)} />
diff --git a/src/utils/commitEntryAction.test.ts b/src/utils/commitEntryAction.test.ts
new file mode 100644
index 00000000..a39342ae
--- /dev/null
+++ b/src/utils/commitEntryAction.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it, vi } from 'vitest'
+import { triggerCommitEntryAction } from './commitEntryAction'
+
+describe('triggerCommitEntryAction', () => {
+ it('runs the automatic checkpoint when AutoGit is enabled', () => {
+ const openCommitDialog = vi.fn().mockResolvedValue(undefined)
+ const runAutomaticCheckpoint = vi.fn().mockResolvedValue(true)
+
+ triggerCommitEntryAction({
+ autoGitEnabled: true,
+ openCommitDialog,
+ runAutomaticCheckpoint,
+ })
+
+ expect(runAutomaticCheckpoint).toHaveBeenCalledWith({ savePendingBeforeCommit: true })
+ expect(openCommitDialog).not.toHaveBeenCalled()
+ })
+
+ it('opens the manual commit dialog when AutoGit is disabled', () => {
+ const openCommitDialog = vi.fn().mockResolvedValue(undefined)
+ const runAutomaticCheckpoint = vi.fn().mockResolvedValue(true)
+
+ triggerCommitEntryAction({
+ autoGitEnabled: false,
+ openCommitDialog,
+ runAutomaticCheckpoint,
+ })
+
+ expect(openCommitDialog).toHaveBeenCalledTimes(1)
+ expect(runAutomaticCheckpoint).not.toHaveBeenCalled()
+ })
+})
diff --git a/src/utils/commitEntryAction.ts b/src/utils/commitEntryAction.ts
new file mode 100644
index 00000000..8d82f277
--- /dev/null
+++ b/src/utils/commitEntryAction.ts
@@ -0,0 +1,21 @@
+type RunAutomaticCheckpoint = (options?: { savePendingBeforeCommit?: boolean }) => Promise
+type OpenCommitDialog = () => Promise
+
+interface CommitEntryActionConfig {
+ autoGitEnabled: boolean
+ openCommitDialog: OpenCommitDialog
+ runAutomaticCheckpoint: RunAutomaticCheckpoint
+}
+
+export function triggerCommitEntryAction({
+ autoGitEnabled,
+ openCommitDialog,
+ runAutomaticCheckpoint,
+}: CommitEntryActionConfig): void {
+ if (autoGitEnabled) {
+ void runAutomaticCheckpoint({ savePendingBeforeCommit: true })
+ return
+ }
+
+ void openCommitDialog()
+}
diff --git a/tests/smoke/manual-commit-modal-autogit-off.spec.ts b/tests/smoke/manual-commit-modal-autogit-off.spec.ts
new file mode 100644
index 00000000..b492a4fa
--- /dev/null
+++ b/tests/smoke/manual-commit-modal-autogit-off.spec.ts
@@ -0,0 +1,168 @@
+import { expect, test, type Page } from '@playwright/test'
+import { executeCommand, openCommandPalette } from './helpers'
+import { seedAutoGitSavedChange } from './testBridge'
+
+type MockHandler = (args?: Record) => unknown
+
+function installCommitFlowMocks() {
+ type BrowserWindow = Window & typeof globalThis & {
+ __gitCommitMessages?: string[]
+ __gitPushCalls?: number
+ __mockHandlers?: Record
+ }
+
+ const browserWindow = window as BrowserWindow
+ const dirtyPaths = new Set()
+ let ahead = 0
+
+ const createModifiedFiles = () => [...dirtyPaths].map((path) => ({
+ path,
+ relativePath: path.split('/').pop() ?? path,
+ status: 'modified',
+ }))
+
+ const isPatched = (handlers?: Record | null) =>
+ !handlers || (handlers as Record).__manualCommitPatched === true
+
+ const patchSaveNoteContent = (handlers: Record) => {
+ const originalSaveNoteContent = handlers.save_note_content
+ handlers.save_note_content = (args?: Record) => {
+ const path = typeof args?.path === 'string' ? args.path : null
+ if (path) dirtyPaths.add(path)
+ return originalSaveNoteContent?.(args)
+ }
+ }
+
+ const patchGitHandlers = (handlers: Record) => {
+ handlers.get_modified_files = () => createModifiedFiles()
+ handlers.git_commit = (args?: Record) => {
+ const message = typeof args?.message === 'string' ? args.message : ''
+ browserWindow.__gitCommitMessages?.push(message)
+ dirtyPaths.clear()
+ ahead = 1
+ return `[main abc1234] ${message}`
+ }
+
+ handlers.git_push = () => {
+ browserWindow.__gitPushCalls = (browserWindow.__gitPushCalls ?? 0) + 1
+ ahead = 0
+ return { status: 'ok', message: 'Pushed to remote' }
+ }
+
+ handlers.git_remote_status = () => ({
+ branch: 'main',
+ ahead,
+ behind: 0,
+ hasRemote: true,
+ })
+ }
+
+ const markPatched = (handlers: Record) => {
+ Object.defineProperty(handlers, '__manualCommitPatched', {
+ configurable: true,
+ enumerable: false,
+ value: true,
+ })
+ }
+
+ const patchHandlers = (handlers?: Record | null) => {
+ if (isPatched(handlers)) {
+ return handlers ?? null
+ }
+
+ patchSaveNoteContent(handlers)
+ patchGitHandlers(handlers)
+ markPatched(handlers)
+ return handlers
+ }
+
+ browserWindow.__gitCommitMessages = []
+ browserWindow.__gitPushCalls = 0
+
+ let ref = patchHandlers(browserWindow.__mockHandlers) ?? null
+ Object.defineProperty(browserWindow, '__mockHandlers', {
+ configurable: true,
+ get() {
+ return patchHandlers(ref) ?? ref
+ },
+ set(value) {
+ ref = patchHandlers(value as Record | undefined) ?? null
+ },
+ })
+}
+
+async function openFirstNote(page: Page) {
+ const noteList = page.locator('[data-testid="note-list-container"]')
+ await noteList.waitFor({ timeout: 5_000 })
+ await noteList.locator('.cursor-pointer').first().click()
+ await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
+}
+
+async function setAutoGitEnabled(page: Page, enabled: boolean) {
+ await openCommandPalette(page)
+ await executeCommand(page, 'Open Settings')
+
+ const settingsPanel = page.getByTestId('settings-panel')
+ await expect(settingsPanel).toBeVisible({ timeout: 5_000 })
+
+ const toggle = page.getByRole('switch', { name: 'AutoGit' })
+ const isEnabled = (await toggle.getAttribute('aria-checked')) === 'true'
+ if (isEnabled !== enabled) {
+ await toggle.click()
+ }
+
+ await page.getByTestId('settings-save').click()
+ await expect(settingsPanel).not.toBeVisible({ timeout: 5_000 })
+}
+
+async function triggerCommitButton(page: Page) {
+ const commitButton = page.getByTestId('status-commit-push')
+ await commitButton.focus()
+ await expect(commitButton).toBeFocused()
+ await page.keyboard.press('Enter')
+}
+
+async function expectCommitMessage(page: Page, index: number, expectedMessage: string) {
+ await expect.poll(async () =>
+ page.evaluate(
+ ({ targetIndex }) => (window as Window & { __gitCommitMessages?: string[] }).__gitCommitMessages?.[targetIndex] ?? '',
+ { targetIndex: index },
+ ),
+ ).toBe(expectedMessage)
+}
+
+async function expectPushCount(page: Page, expectedCount: number) {
+ await expect.poll(async () =>
+ page.evaluate(() => (window as Window & { __gitPushCalls?: number }).__gitPushCalls ?? 0),
+ ).toBe(expectedCount)
+}
+
+test('@smoke commit entry opens the manual modal when AutoGit is off and switches back immediately when enabled', async ({ page }) => {
+ await page.addInitScript(installCommitFlowMocks)
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ await openFirstNote(page)
+ await setAutoGitEnabled(page, false)
+ await seedAutoGitSavedChange(page)
+ await triggerCommitButton(page)
+
+ await expect(page.getByRole('heading', { name: 'Commit & Push' })).toBeVisible()
+ const messageInput = page.locator('textarea[placeholder="Commit message..."]')
+ await expect(messageInput).toBeFocused()
+ await page.keyboard.press('Meta+A')
+ await page.keyboard.type('Manual commit from keyboard')
+ await page.keyboard.press('Meta+Enter')
+
+ await expect(page.locator('.fixed.bottom-8')).toContainText('Committed and pushed', { timeout: 5_000 })
+ await expectCommitMessage(page, 0, 'Manual commit from keyboard')
+ await expectPushCount(page, 1)
+
+ await setAutoGitEnabled(page, true)
+ await seedAutoGitSavedChange(page)
+ await triggerCommitButton(page)
+
+ await expect(messageInput).not.toBeVisible()
+ await expectCommitMessage(page, 1, 'Updated 1 note')
+ await expectPushCount(page, 2)
+})