import type { ActionFn, AppAction, KeyResult, ModeContext, ModeId } from './types' import { isAutoCommitEnabled } from './auto-commit-runtime' const AUTO_COMMIT_DISABLED_MESSAGE = 'auto-commit is disabled — set autoCommit: { enabled: true } in aimux.config.ts' function r( actions: KeyResult['actions'] = [], effects: KeyResult['effects'] = [], transition?: KeyResult['transition'] ): KeyResult { return transition ? { actions, effects, transition } : { actions, effects } } // --------------------------------------------------------------------------- // Static actions (KeyResult values — no ctx needed) // --------------------------------------------------------------------------- export const nextTab: KeyResult = r([{ delta: 1, type: 'move-active-tab' }]) export const prevTab: KeyResult = r([{ delta: -1, type: 'move-active-tab' }]) export const newTab: KeyResult = r( [{ type: 'open-new-tab-modal' }], [], 'modal.new-tab.command-edit' ) export const renameTab: KeyResult = r([{ type: 'open-rename-tab-modal' }], [], 'modal.rename-tab') export const sessionPicker: KeyResult = r( [{ type: 'open-session-picker' }], [], 'modal.session-picker.filtering' ) export const snippetPicker: KeyResult = r( [{ type: 'open-snippet-picker' }], [], 'modal.snippet-picker.filtering' ) export const themePicker: KeyResult = r( [{ type: 'open-theme-picker' }], [{ action: 'open', type: 'apply-theme' }], 'modal.theme-picker.filtering' ) export function helpModal(scope?: ModeId): KeyResult { return r([{ scope, type: 'open-help-modal' }]) } export const openFlashJump: KeyResult = r( [{ type: 'open-flash-jump-modal' }], [], 'modal.flash-jump' ) export const toggleSidebar: KeyResult = r([{ type: 'toggle-sidebar' }]) export const toggleGitPane: KeyResult = r([{ type: 'toggle-git-pane' }]) export const toggleSessionBar: KeyResult = r([{ type: 'toggle-session-bar' }]) export const toggleAIUsage: ActionFn = (ctx: ModeContext) => { if (ctx.state.modal.type === 'ai-usage') { return r([{ type: 'close-modal' }], [], 'navigation') } return r([{ type: 'open-ai-usage-modal' }], [], 'modal.ai-usage') } export function setGitPaneMode(mode: 'embedded' | 'pane'): KeyResult { return r([{ mode, type: 'set-git-pane-mode' }]) } export function setGitPanePosition(position: 'top' | 'bottom' | 'left' | 'right'): KeyResult { return r([{ position, type: 'set-git-pane-position' }]) } export const enterGitMode: KeyResult = r([{ type: 'enter-git-mode' }], [], 'git-mode') export function switchSessionByIndex(index: number): KeyResult { return r([], [{ index, type: 'switch-session-by-index' }]) } export function switchTabByIndex(index: number): KeyResult { return r([], [{ index, type: 'switch-tab-by-index' }]) } export const nextSidebarItem: KeyResult = r([], [{ direction: 1, type: 'cycle-sidebar-item' }]) export const prevSidebarItem: KeyResult = r([], [{ direction: -1, type: 'cycle-sidebar-item' }]) export const splitVertical: KeyResult = r( [{ direction: 'vertical', type: 'open-split-picker' }], [], 'modal.split-picker' ) export const splitHorizontal: KeyResult = r( [{ direction: 'horizontal', type: 'open-split-picker' }], [], 'modal.split-picker' ) export const enterInsert: ActionFn = (ctx: ModeContext) => { if (!(ctx.state.activeTabId != null && ctx.state.activeTabId !== '')) return null return r([{ focusMode: 'terminal-input', type: 'set-focus-mode' }], [], 'terminal-input') } export const closeModal: KeyResult = r([{ type: 'close-modal' }], [], 'navigation') export const closeOverlayModal: KeyResult = r([{ type: 'close-modal' }]) export const cancelNewTabModal: KeyResult = r([{ type: 'cancel-command-edit' }]) // --------------------------------------------------------------------------- // Dynamic actions (need ctx at runtime — ActionFn) // --------------------------------------------------------------------------- export const closeTab: ActionFn = (ctx: ModeContext) => { const tabId = ctx.state.activeTabId if (!(tabId != null && tabId !== '')) return null return r([{ type: 'close-active-tab' }], [{ tabId, type: 'close-tab' }]) } export const restartTab: ActionFn = (ctx: ModeContext) => { const tab = ctx.state.tabs.find((t) => t.id === ctx.state.activeTabId) if (!tab) return null return r([], [{ tab, type: 'restart-tab' }]) } export const quit: ActionFn = (ctx: ModeContext) => { return r([], [{ state: ctx.state, type: 'quit' }]) } // --------------------------------------------------------------------------- // Parameterized factories // --------------------------------------------------------------------------- export function moveTab(delta: number): KeyResult { return r([{ delta, type: 'move-active-tab' }]) } export function reorderTab(delta: number): KeyResult { return r([{ delta, type: 'reorder-active-tab' }]) } export function reorderSession(delta: number): KeyResult { return r([{ delta, type: 'reorder-active-session' }]) } export function resizeSidebar(delta: number): KeyResult { return r([{ delta, type: 'resize-sidebar' }]) } export function resizeGitPane(delta: number): KeyResult { return r([{ delta, type: 'resize-git-pane' }]) } export function resizeGitDiffPane(delta: number): ActionFn { return (ctx: ModeContext) => { const nextRatio = Math.max(0.2, Math.min(0.8, ctx.state.gitPane.diffModeRatio + delta)) if (nextRatio === ctx.state.gitPane.diffModeRatio) return r([]) return r( [{ delta, type: 'resize-git-diff-pane' }], [{ ratio: nextRatio, type: 'persist-git-diff-mode-ratio' }] ) } } export function focusPane(direction: 'left' | 'right' | 'up' | 'down'): KeyResult { return r( [ { direction, type: 'focus-pane-direction' }, { focusMode: 'terminal-input', type: 'set-focus-mode' }, ], [], 'terminal-input' ) } export function resizePane(delta: number, axis: 'horizontal' | 'vertical'): ActionFn { return (ctx: ModeContext) => { const tabId = ctx.state.activeTabId if (!(tabId != null && tabId !== '')) return null return r([{ axis, delta, tabId, type: 'resize-pane' }]) } } export function moveModalSelection(delta: number): KeyResult { return r([{ delta, type: 'move-modal-selection' }]) } export function moveModalSelectionWithPreview( delta: number, effects: KeyResult['effects'] ): KeyResult { return r([{ delta, type: 'move-modal-selection' }], effects) } // --------------------------------------------------------------------------- // Modal-specific actions // --------------------------------------------------------------------------- export const launchSelectedAssistant: ActionFn = (ctx: ModeContext) => { if (ctx.state.modal.type === 'new-tab' && ctx.state.modal.step === 'assistant') { return r([{ type: 'select-new-tab-assistant' }]) } if ( ctx.state.modal.type === 'new-tab' && ctx.state.modal.step === 'worktree' && ctx.state.modal.createWorktree ) { return r([{ type: 'enter-new-tab-worktree-create' }], [{ type: 'load-new-tab-base-branches' }]) } return r([], [{ type: 'launch-selected-assistant' }]) } export const toggleNewTabWorktree: KeyResult = r([{ type: 'toggle-new-tab-worktree' }]) export const deleteSelectedWorktree: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal const sessionId = ctx.state.currentSessionId if (modal.type !== 'new-tab' || modal.step !== 'worktree' || modal.createWorktree) return null if (!(sessionId != null && sessionId !== '')) return null const session = ctx.state.sessions.find((entry) => entry.id === sessionId) const worktree = session?.worktrees?.[modal.selectedIndex] if (!worktree) return null // First attempt is never forced — a recoverable failure opens the confirm // dialog (see set-new-tab-worktree-delete-prompt), where confirmDeleteWorktree // retries with force. return r( [{ index: modal.selectedIndex, type: 'set-modal-selection-index' }], [{ force: false, sessionId, type: 'delete-worktree', worktreeId: worktree.id }] ) } export const confirmDeleteWorktree: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal const sessionId = ctx.state.currentSessionId if (modal.type !== 'new-tab' || modal.worktreeDeletePrompt == null) return null if (!(sessionId != null && sessionId !== '')) return null const { worktreeId } = modal.worktreeDeletePrompt return r( [{ prompt: null, type: 'set-new-tab-worktree-delete-prompt' }], [{ force: true, sessionId, type: 'delete-worktree', worktreeId }], 'modal.new-tab.command-edit' ) } export const cancelDeleteWorktree: KeyResult = r( [{ prompt: null, type: 'set-new-tab-worktree-delete-prompt' }], [], 'modal.new-tab.command-edit' ) export const confirmWorktreeDeleteModal: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal if (modal.type !== 'worktree-delete-confirm') return null return r( [{ type: 'close-modal' }], [ { closeTabs: modal.closeTabs, force: modal.force, sessionId: modal.sessionId, type: 'delete-worktree', worktreeId: modal.worktreeId, }, ], 'navigation' ) } export const cancelCommandEdit = (returnTo: KeyResult['transition']): KeyResult => r([{ type: 'cancel-command-edit' }], [], returnTo) export const confirmSelectedSession: KeyResult = r([], [{ type: 'confirm-selected-session' }]) export const openCreateSessionModal: KeyResult = r( [{ returnToSessionPicker: true, type: 'open-create-session-modal' }], [], 'modal.create-session' ) export const openRenameSelectedSession: KeyResult = r( [], [{ type: 'open-rename-selected-session' }] ) export const deleteSelectedSession: KeyResult = r([], [{ type: 'delete-selected-session' }]) export const openSnippetEditor: KeyResult = r( [{ type: 'open-snippet-editor' }], [], 'modal.snippet-editor' ) export const editSelectedSnippet: KeyResult = r([], [{ type: 'edit-selected-snippet' }]) export const deleteSelectedSnippet: KeyResult = r([], [{ type: 'delete-selected-snippet' }]) export const openSelectedSnippetSourceInEditor: KeyResult = r( [], [{ type: 'open-selected-snippet-source-in-editor' }] ) export const pasteSelectedSnippet: KeyResult = r( [{ type: 'close-modal' }], [{ type: 'paste-selected-snippet' }], 'navigation' ) export const pasteSnippetToGroup: KeyResult = r( [{ type: 'close-modal' }], [{ type: 'paste-snippet-to-group' }], 'navigation' ) export const confirmSplit: KeyResult = r([], [{ type: 'confirm-split' }]) export const restoreTheme: KeyResult = r( [{ type: 'close-modal' }], [{ action: 'restore', type: 'apply-theme' }], 'navigation' ) export const confirmTheme: KeyResult = r( [{ type: 'close-modal' }], [{ action: 'confirm', type: 'apply-theme' }], 'navigation' ) export function previewTheme(delta: 1 | -1): KeyResult { return r( [{ delta, type: 'move-modal-selection' }], [{ action: 'preview', delta, type: 'apply-theme' }] ) } export const toggleTransparent: KeyResult = r([], [{ type: 'toggle-transparent' }]) export const toggleMode: KeyResult = r([], [{ type: 'toggle-mode' }]) export const switchField: KeyResult = r([{ type: 'switch-create-session-field' }]) export const selectDirectory: KeyResult = r([{ type: 'select-directory' }]) export const backToSessionPicker: KeyResult = r( [{ type: 'open-session-picker' }], [], 'modal.session-picker.filtering' ) export const createSessionEscape: ActionFn = (ctx: ModeContext) => { if (ctx.state.modal.type === 'create-session' && !ctx.state.modal.returnToSessionPicker) { return r([{ type: 'close-modal' }], [], 'navigation') } return r([{ type: 'open-session-picker' }], [], 'modal.session-picker.filtering') } export const backToSnippetPicker: KeyResult = r( [{ type: 'open-snippet-picker' }], [], 'modal.snippet-picker.filtering' ) export const saveSnippetEditor: KeyResult = r( [{ type: 'close-modal' }], [{ type: 'save-snippet-editor' }], 'navigation' ) export const saveCustomCommand: KeyResult = r( [], [{ type: 'save-custom-command' }], 'modal.new-tab.command-edit' ) export const cancelEditCustomCommand: KeyResult = r( [{ type: 'cancel-command-edit' }], [], 'modal.new-tab.command-edit' ) export const editSelectedAssistant: KeyResult = r( [], [{ type: 'edit-selected-assistant' }], 'modal.new-tab.editing-command' ) // --------------------------------------------------------------------------- // Update-available modal // --------------------------------------------------------------------------- export const confirmUpdateSelection: KeyResult = r( [{ type: 'close-modal' }], [{ type: 'confirm-update-selection' }], 'navigation' ) // --------------------------------------------------------------------------- // Worktree-move modal // --------------------------------------------------------------------------- export const openWorktreeMove: ActionFn = (ctx: ModeContext) => { if (ctx.state.gitMode.headOffset > 0) { return r([ { message: 'disabled while viewing HEAD~N (press 0 or [ to return)', type: 'git-mode-set-message', }, ]) } const session = ctx.state.sessions.find((entry) => entry.id === ctx.state.currentSessionId) const worktrees = session?.worktrees ?? [] // From git mode, the source is the active worktree (the one being reviewed). const source = worktrees.find((w) => w.id === session?.activeWorktreeId) ?? worktrees[0] const hasBranch = source != null && source.branch != null && source.branch !== '' const others = worktrees.filter((w) => w.id !== source?.id) if (!hasBranch || source == null || others.length < 1) { return r([{ message: 'no other worktree to move into', type: 'git-mode-set-message' }]) } // Overlay: no mode transition — deriveModeId routes input to the picker and // focusMode stays 'git' so the git view remains mounted underneath. return r( [{ sourceWorktreeId: source.id, type: 'open-worktree-move-modal' }], [{ type: 'load-worktree-move-stats' }] ) } export const toggleWorktreeMoveDelete: KeyResult = r([{ type: 'toggle-worktree-move-delete' }]) export const confirmWorktreeMove: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal const session = ctx.state.sessions.find((entry) => entry.id === ctx.state.currentSessionId) const worktrees = session?.worktrees ?? [] const sourceId = modal.type === 'worktree-move' ? modal.sourceWorktreeId : undefined const source = worktrees.find((w) => w.id === sourceId) const targets = worktrees.filter((w) => w.id !== sourceId) const selectedIndex = modal.type === 'worktree-move' ? modal.selectedIndex : 0 const target = targets[selectedIndex] if (!target || !session || !source || modal.type !== 'worktree-move') { return r([{ type: 'close-modal' }]) } // Overlay close keeps focusMode 'git' (see close-modal reducer), so the move // result lands back in the git view that was underneath the picker. return r( [{ type: 'close-modal' }], [ { deleteSource: modal.deleteSource, sessionId: session.id, sourceWorktreeId: source.id, targetWorktreeId: target.id, type: 'move-worktree', }, ] ) } // Confirms the dialog opened after a recoverable move failure: re-runs the // move with the flag matching the failure (stash the target / keep conflicts). export const confirmWorktreeMoveRetry: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal if (modal.type !== 'worktree-move-confirm') return null return r( [{ type: 'close-modal' }], [ { deleteSource: modal.deleteSource, sessionId: modal.sessionId, sourceWorktreeId: modal.sourceWorktreeId, targetWorktreeId: modal.targetWorktreeId, type: 'move-worktree', ...(modal.variant === 'stash-target' ? { stashTarget: true } : { keepConflicts: true }), }, ], 'navigation' ) } export const closePane: ActionFn = (ctx: ModeContext) => { const tabId = ctx.state.activeTabId if (!(tabId != null && tabId !== '')) return null return r( [ { tabId, type: 'close-pane' }, { focusMode: 'terminal-input', type: 'set-focus-mode' }, ], [{ tabId, type: 'close-tab' }], 'terminal-input' ) } // Navigation-specific dynamic actions export const ctrlZSidebar: ActionFn = (ctx: ModeContext) => { if (!ctx.state.sidebar.visible) { return r([{ type: 'toggle-sidebar' }]) } return r([]) } // --------------------------------------------------------------------------- // Terminal-input escape shortcuts (used by raw-input-handler) // These fire while the user is actively in terminal-input mode. // --------------------------------------------------------------------------- export const leaveTerminalInput: KeyResult = r( [{ focusMode: 'navigation', type: 'set-focus-mode' }], [], 'navigation' ) export const toggleSidebarFromInput: KeyResult = r([{ type: 'toggle-sidebar' }]) // Session name modal export const confirmSessionRename: ActionFn = (ctx: ModeContext) => { const trimmed = (ctx.state.modal.editBuffer ?? '').trim() const sessionId = ctx.state.modal.sessionTargetId const returnToPicker = ctx.state.modal.type === 'session-name' ? ctx.state.modal.returnToSessionPicker : true const closeAction: AppAction = returnToPicker ? { type: 'open-session-picker' } : { type: 'close-modal' } const transition: KeyResult['transition'] = returnToPicker ? 'modal.session-picker.filtering' : 'navigation' const effects: KeyResult['effects'] = trimmed && sessionId != null && sessionId !== '' ? [{ name: trimmed, sessionId, type: 'rename-session' }] : [] return r([closeAction], effects, transition) } // Rename tab modal export const confirmRenameTab: ActionFn = (ctx: ModeContext) => { const trimmed = (ctx.state.modal.editBuffer ?? '').trim() const tabId = ctx.state.modal.sessionTargetId const actions: KeyResult['actions'] = [] const effects: KeyResult['effects'] = [] if (trimmed && tabId != null && tabId !== '') { effects.push({ tabId, title: trimmed, type: 'rename-tab' }) } actions.push({ type: 'close-modal' }) return r(actions, effects, 'navigation') } // Rename worktree modal export const confirmRenameWorktree: ActionFn = (ctx: ModeContext) => { const { modal } = ctx.state const trimmed = (modal.editBuffer ?? '').trim() const worktreeId = modal.sessionTargetId const sessionId = modal.type === 'rename-worktree' ? modal.worktreeSessionId : null const actions: KeyResult['actions'] = [] if (trimmed && worktreeId != null && worktreeId !== '' && sessionId != null && sessionId !== '') { actions.push({ patch: { name: trimmed }, sessionId, type: 'update-worktree-record', worktreeId, }) } actions.push({ type: 'close-modal' }) return r(actions, [], 'navigation') } // Create session modal export const confirmCreateSession: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal as { activeField?: string editBuffer?: string pendingProjectPath?: string } if (modal.activeField === 'directory') { return r([{ type: 'select-directory' }]) } const trimmed = (modal.editBuffer ?? '').trim() const projectPath = modal.pendingProjectPath ?? undefined const sessionName = trimmed || getDefaultSessionName(projectPath) if (sessionName) { return r( [{ type: 'close-modal' }], [{ name: sessionName, projectPath, type: 'create-session' }], 'navigation' ) } return r([{ type: 'close-modal' }], [], 'navigation') } function getDefaultSessionName(projectPath?: string): string { if (!(projectPath != null && projectPath !== '')) return '' const segments = projectPath.split('/').filter(Boolean) return segments.at(-1) ?? '' } // Session picker escape (conditional) export const sessionPickerEscape: ActionFn = (ctx: ModeContext) => { if (!(ctx.state.currentSessionId != null && ctx.state.currentSessionId !== '')) return null return r([{ type: 'close-modal' }], [], 'navigation') } // Snippet filter: paste from filter + close export const snippetFilterPaste: KeyResult = r( [{ type: 'close-modal' }], [{ type: 'paste-selected-snippet' }], 'navigation' ) export const snippetFilterPasteToGroup: KeyResult = r( [{ type: 'close-modal' }], [{ type: 'paste-snippet-to-group' }], 'navigation' ) // --------------------------------------------------------------------------- // Git mode (src/input/modes/handlers/git-mode.ts replacement) // --------------------------------------------------------------------------- function clearPendingDelete(ctx: ModeContext): AppAction[] { if (ctx.state.gitMode.pendingDeletePath === null) return [] return [{ path: null, type: 'git-mode-set-pending-delete' }] } function gitFileKey(section: string, path: string, repoPath?: string): string { return repoPath != null && repoPath !== '' ? `${section}:${repoPath}:${path}` : `${section}:${path}` } function selectedGitFile(ctx: ModeContext) { const key = ctx.state.gitMode.selectedEntryKey if (!(key != null && key !== '')) return null return ( ctx.state.gitPanel.files.find( (file) => gitFileKey(file.section, file.path, file.repoPath) === key ) ?? null ) } export const exitGitMode: KeyResult = r([{ type: 'exit-git-mode' }], [], 'navigation') export function selectGitFile(delta: -1 | 1): ActionFn { return (ctx: ModeContext) => { if (ctx.state.gitPanel.files.length === 0) return r([]) return r([{ delta, type: 'git-mode-move-selection' }]) } } export function selectGitFileOnly(delta: -1 | 1): ActionFn { return (ctx: ModeContext) => { if (ctx.state.gitPanel.files.length === 0) return r([]) return r([{ delta, type: 'git-mode-move-file-selection' }]) } } export const toggleSelectedGitFolder: ActionFn = (ctx: ModeContext) => { if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== '')) return r([]) return r([{ type: 'git-mode-toggle-selected-folder' }]) } export const collapseGitSelection: ActionFn = (ctx: ModeContext) => { if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== '')) return r([]) return r([{ type: 'git-mode-collapse-selection' }]) } export const expandGitSelection: ActionFn = (ctx: ModeContext) => { if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== '')) return r([]) return r([{ type: 'git-mode-expand-selection' }]) } export const toggleGitFileListMode: ActionFn = (ctx: ModeContext) => { const mode = ctx.state.gitPane.fileListMode === 'tree' ? 'flat' : 'tree' return r( [{ type: 'git-mode-toggle-file-list-mode' }], [{ mode, type: 'persist-git-file-list-mode' }] ) } export const toggleTreeCompaction: ActionFn = (ctx: ModeContext) => { const enabled = !ctx.state.gitPane.treeCompaction return r( [{ type: 'git-mode-toggle-tree-compaction' }], [{ enabled, type: 'persist-git-tree-compaction' }] ) } export function scrollGitDiff(delta: number): KeyResult { return r([], [{ delta, type: 'scroll-git-diff' }]) } export const toggleGitDiffView: KeyResult = r([{ type: 'git-mode-toggle-diff-view' }]) export const toggleGitReviewBase: KeyResult = r([{ type: 'git-mode-toggle-review-base' }]) export function shiftGitHeadOffset(delta: number): KeyResult { return r([ { delta, type: 'git-mode-shift-head-offset' }, { message: null, type: 'git-mode-set-message' }, ]) } export const gitStageSelected: ActionFn = (ctx: ModeContext) => { if (ctx.state.gitMode.headOffset > 0) { return r([ { message: 'disabled while viewing HEAD~N (press 0 or [ to return)', type: 'git-mode-set-message', }, ]) } const file = selectedGitFile(ctx) if (!file) return r(clearPendingDelete(ctx)) const actions = clearPendingDelete(ctx) if (file.section === 'staged') return r(actions) actions.push({ fromSection: file.section, path: file.path, toSection: 'staged', type: 'git-mode-optimistic-move', }) return r(actions, [{ path: file.path, type: 'git-stage' }]) } export const gitDestructiveSelected: ActionFn = (ctx: ModeContext) => { if (ctx.state.gitMode.headOffset > 0) { return r([ { message: 'disabled while viewing HEAD~N (press 0 or [ to return)', type: 'git-mode-set-message', }, ]) } const file = selectedGitFile(ctx) if (!file) return r(clearPendingDelete(ctx)) if (file.section === 'staged') { const actions = clearPendingDelete(ctx) const toSection = file.status === 'A' ? 'untracked' : 'unstaged' actions.push({ fromSection: 'staged', path: file.path, toSection, type: 'git-mode-optimistic-move', }) return r(actions, [{ path: file.path, type: 'git-unstage' }]) } const isUntracked = file.section === 'untracked' const pending = ctx.state.gitMode.pendingDeletePath if (pending === file.path) { const actions: AppAction[] = [ { path: null, type: 'git-mode-set-pending-delete' }, { fromSection: file.section, path: file.path, toSection: null, type: 'git-mode-optimistic-move', }, ] const effectType = isUntracked ? 'git-rm' : 'git-restore' return r(actions, [{ path: file.path, type: effectType }]) } return r([{ path: file.path, type: 'git-mode-set-pending-delete' }]) } export const gitToggleFoldAll: ActionFn = (ctx: ModeContext) => { const file = selectedGitFile(ctx) if (!file) return r([]) const key = gitFileKey(file.section, file.path, file.repoPath) return r([{ key, type: 'git-mode-fold-toggle-all' }]) } export const gitStageAll: ActionFn = (ctx: ModeContext) => { if (ctx.state.gitMode.headOffset > 0) { return r([ { message: 'disabled while viewing HEAD~N (press 0 or [ to return)', type: 'git-mode-set-message', }, ]) } const files = ctx.state.gitPanel.files const pending = clearPendingDelete(ctx) if (files.length === 0) return r(pending) const actions: AppAction[] = [...pending] for (const f of files) { if (f.section === 'staged') continue actions.push({ fromSection: f.section, path: f.path, toSection: 'staged', type: 'git-mode-optimistic-move', }) } return r(actions, [{ type: 'git-stage-all' }]) } export const gitUnstageAll: ActionFn = (ctx: ModeContext) => { if (ctx.state.gitMode.headOffset > 0) { return r([ { message: 'disabled while viewing HEAD~N (press 0 or [ to return)', type: 'git-mode-set-message', }, ]) } const files = ctx.state.gitPanel.files const pending = clearPendingDelete(ctx) if (files.length === 0) return r(pending) const actions: AppAction[] = [...pending] for (const f of files) { if (f.section !== 'staged') continue const toSection = f.status === 'A' ? 'untracked' : 'unstaged' actions.push({ fromSection: 'staged', path: f.path, toSection, type: 'git-mode-optimistic-move', }) } return r(actions, [{ type: 'git-unstage-all' }]) } export const gitCommitOpen: ActionFn = (ctx: ModeContext) => { if (ctx.state.gitMode.headOffset > 0) { return r([ { message: 'disabled while viewing HEAD~N (press 0 or [ to return)', type: 'git-mode-set-message', }, ]) } const sessionId = ctx.state.currentSessionId ?? undefined return r( [...clearPendingDelete(ctx), { sessionId, type: 'open-git-commit-modal' }], [], 'modal.git-commit' ) } export const gitPush: ActionFn = (ctx: ModeContext) => { if (ctx.state.gitMode.headOffset > 0) { return r([ { message: 'disabled while viewing HEAD~N (press 0 or [ to return)', type: 'git-mode-set-message', }, ]) } return r(clearPendingDelete(ctx), [{ type: 'git-push' }]) } export const openSelectedGitFileInEditor: ActionFn = (ctx: ModeContext) => { const file = selectedGitFile(ctx) if (!file) return r([]) return r([], [{ path: file.path, type: 'open-file-in-editor' }]) } // --------------------------------------------------------------------------- // Modal: git-commit // --------------------------------------------------------------------------- function extractGitCommitFields(modal: ModeContext['state']['modal']): { title: string body: string } | null { if (modal.type !== 'git-commit') return null const editBuffer = modal.editBuffer ?? '' const activeIsTitle = modal.activeField === 'title' const title = (activeIsTitle ? editBuffer : modal.contentBuffer).trim() const body = (activeIsTitle ? modal.contentBuffer : editBuffer).trim() return { body, title } } export const gitCommitCancel: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal if (modal.type === 'git-commit' && modal.stage === 'confirm') { return r([{ type: 'git-commit-leave-confirm' }], [], 'modal.git-commit') } return r([{ type: 'close-modal' }], [], 'git-mode') } export const gitCommitEnterConfirm: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal if (modal.type !== 'git-commit') return null if (!isAutoCommitEnabled()) { return r([{ message: AUTO_COMMIT_DISABLED_MESSAGE, type: 'git-mode-set-message' }]) } const fields = extractGitCommitFields(modal) const hasTitle = !!fields && fields.title.length > 0 if (hasTitle) { return r([{ type: 'git-commit-enter-confirm' }], [], 'modal.git-commit.confirm') } const sessionId = ctx.state.currentSessionId if (!(sessionId != null && sessionId !== '')) { return r([{ type: 'git-commit-enter-confirm' }], [], 'modal.git-commit.confirm') } return r( [{ sessionId, type: 'git-commit-enter-generating' }], [{ sessionId, type: 'generate-auto-commit-now' }], 'modal.git-commit.generating' ) } export const gitCommitLeaveConfirm: KeyResult = r( [{ type: 'git-commit-leave-confirm' }], [], 'modal.git-commit' ) export const gitCommitLeaveGenerating: ActionFn = (ctx: ModeContext) => { const sessionId = ctx.state.currentSessionId const actionsList: AppAction[] = [{ type: 'git-commit-leave-generating' }] if (sessionId != null && sessionId !== '') { actionsList.push({ sessionId, type: 'auto-commit-clear' }) } return r(actionsList, [], 'modal.git-commit') } export const gitCommitAutoAccept: ActionFn = (ctx: ModeContext) => { if (!isAutoCommitEnabled()) { return r([{ message: AUTO_COMMIT_DISABLED_MESSAGE, type: 'git-mode-set-message' }]) } const fields = extractGitCommitFields(ctx.state.modal) if (!fields) { return r([{ type: 'close-modal' }], [], 'git-mode') } return r( [{ type: 'close-modal' }], [{ body: fields.body, title: fields.title, type: 'git-commit-auto' }], 'git-mode' ) } export const gitCommitSubmit: ActionFn = (ctx: ModeContext) => { const fields = extractGitCommitFields(ctx.state.modal) if (!fields) { return r([{ type: 'close-modal' }], [], 'git-mode') } return r( [{ type: 'close-modal' }], [{ body: fields.body, title: fields.title, type: 'git-commit' }], 'git-mode' ) } export const gitCommitReturnKey: ActionFn = (ctx: ModeContext) => { const modal = ctx.state.modal if (modal.type === 'git-commit' && modal.activeField === 'body') { return r([{ char: '\n', type: 'update-command-edit' }]) } return r([{ type: 'switch-create-session-field' }]) }