| |
| |
| |
| |
| |
| |
| import { useCallback, useRef } from 'react' |
| import { logEvent } from 'src/services/analytics/index.js' |
| import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js' |
| import { |
| useAppState, |
| useAppStateStore, |
| useSetAppState, |
| } from 'src/state/AppState.js' |
| import { isVimModeEnabled } from '../components/PromptInput/utils.js' |
| import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js' |
| import type { SpinnerMode } from '../components/Spinner/types.js' |
| import { useNotifications } from '../context/notifications.js' |
| import { useIsOverlayActive } from '../context/overlayContext.js' |
| import { useCommandQueue } from '../hooks/useCommandQueue.js' |
| import { getShortcutDisplay } from '../keybindings/shortcutFormat.js' |
| import { useKeybinding } from '../keybindings/useKeybinding.js' |
| import type { Screen } from '../screens/REPL.js' |
| import { exitTeammateView } from '../state/teammateViewHelpers.js' |
| import { |
| killAllRunningAgentTasks, |
| markAgentsNotified, |
| } from '../tasks/LocalAgentTask/LocalAgentTask.js' |
| import type { PromptInputMode, VimMode } from '../types/textInputTypes.js' |
| import { |
| clearCommandQueue, |
| enqueuePendingNotification, |
| hasCommandsInQueue, |
| } from '../utils/messageQueueManager.js' |
| import { emitTaskTerminatedSdk } from '../utils/sdkEventQueue.js' |
|
|
| |
| const KILL_AGENTS_CONFIRM_WINDOW_MS = 3000 |
|
|
| type CancelRequestHandlerProps = { |
| setToolUseConfirmQueue: ( |
| f: (toolUseConfirmQueue: ToolUseConfirm[]) => ToolUseConfirm[], |
| ) => void |
| onCancel: () => void |
| onAgentsKilled: () => void |
| isMessageSelectorVisible: boolean |
| screen: Screen |
| abortSignal?: AbortSignal |
| popCommandFromQueue?: () => void |
| vimMode?: VimMode |
| isLocalJSXCommand?: boolean |
| isSearchingHistory?: boolean |
| isHelpOpen?: boolean |
| inputMode?: PromptInputMode |
| inputValue?: string |
| streamMode?: SpinnerMode |
| } |
|
|
| |
| |
| |
| |
| export function CancelRequestHandler(props: CancelRequestHandlerProps): null { |
| const { |
| setToolUseConfirmQueue, |
| onCancel, |
| onAgentsKilled, |
| isMessageSelectorVisible, |
| screen, |
| abortSignal, |
| popCommandFromQueue, |
| vimMode, |
| isLocalJSXCommand, |
| isSearchingHistory, |
| isHelpOpen, |
| inputMode, |
| inputValue, |
| streamMode, |
| } = props |
| const store = useAppStateStore() |
| const setAppState = useSetAppState() |
| const queuedCommandsLength = useCommandQueue().length |
| const { addNotification, removeNotification } = useNotifications() |
| const lastKillAgentsPressRef = useRef<number>(0) |
| const viewSelectionMode = useAppState(s => s.viewSelectionMode) |
|
|
| const handleCancel = useCallback(() => { |
| const cancelProps = { |
| source: |
| 'escape' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| streamMode: |
| streamMode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } |
|
|
| |
| |
| if (abortSignal !== undefined && !abortSignal.aborted) { |
| logEvent('tengu_cancel', cancelProps) |
| setToolUseConfirmQueue(() => []) |
| onCancel() |
| return |
| } |
|
|
| |
| if (hasCommandsInQueue()) { |
| if (popCommandFromQueue) { |
| popCommandFromQueue() |
| return |
| } |
| } |
|
|
| |
| logEvent('tengu_cancel', cancelProps) |
| setToolUseConfirmQueue(() => []) |
| onCancel() |
| }, [ |
| abortSignal, |
| popCommandFromQueue, |
| setToolUseConfirmQueue, |
| onCancel, |
| streamMode, |
| ]) |
|
|
| |
| |
| |
| |
| const isOverlayActive = useIsOverlayActive() |
| const canCancelRunningTask = abortSignal !== undefined && !abortSignal.aborted |
| const hasQueuedCommands = queuedCommandsLength > 0 |
| |
| |
| |
| const isInSpecialModeWithEmptyInput = |
| inputMode !== undefined && inputMode !== 'prompt' && !inputValue |
| |
| const isViewingTeammate = viewSelectionMode === 'viewing-agent' |
| |
| const isContextActive = |
| screen !== 'transcript' && |
| !isSearchingHistory && |
| !isMessageSelectorVisible && |
| !isLocalJSXCommand && |
| !isHelpOpen && |
| !isOverlayActive && |
| !(isVimModeEnabled() && vimMode === 'INSERT') |
|
|
| |
| |
| const isEscapeActive = |
| isContextActive && |
| (canCancelRunningTask || hasQueuedCommands) && |
| !isInSpecialModeWithEmptyInput && |
| !isViewingTeammate |
|
|
| |
| |
| |
| |
| const isCtrlCActive = |
| isContextActive && |
| (canCancelRunningTask || hasQueuedCommands || isViewingTeammate) |
|
|
| useKeybinding('chat:cancel', handleCancel, { |
| context: 'Chat', |
| isActive: isEscapeActive, |
| }) |
|
|
| |
| |
| |
| const killAllAgentsAndNotify = useCallback((): boolean => { |
| const tasks = store.getState().tasks |
| const running = Object.entries(tasks).filter( |
| ([, t]) => t.type === 'local_agent' && t.status === 'running', |
| ) |
| if (running.length === 0) return false |
| killAllRunningAgentTasks(tasks, setAppState) |
| const descriptions: string[] = [] |
| for (const [taskId, task] of running) { |
| markAgentsNotified(taskId, setAppState) |
| descriptions.push(task.description) |
| emitTaskTerminatedSdk(taskId, 'stopped', { |
| toolUseId: task.toolUseId, |
| summary: task.description, |
| }) |
| } |
| const summary = |
| descriptions.length === 1 |
| ? `Background agent "${descriptions[0]}" was stopped by the user.` |
| : `${descriptions.length} background agents were stopped by the user: ${descriptions.map(d => `"${d}"`).join(', ')}.` |
| enqueuePendingNotification({ value: summary, mode: 'task-notification' }) |
| onAgentsKilled() |
| return true |
| }, [store, setAppState, onAgentsKilled]) |
|
|
| |
| |
| |
| const handleInterrupt = useCallback(() => { |
| if (isViewingTeammate) { |
| killAllAgentsAndNotify() |
| exitTeammateView(setAppState) |
| } |
| if (canCancelRunningTask || hasQueuedCommands) { |
| handleCancel() |
| } |
| }, [ |
| isViewingTeammate, |
| killAllAgentsAndNotify, |
| setAppState, |
| canCancelRunningTask, |
| hasQueuedCommands, |
| handleCancel, |
| ]) |
|
|
| useKeybinding('app:interrupt', handleInterrupt, { |
| context: 'Global', |
| isActive: isCtrlCActive, |
| }) |
|
|
| |
| |
| |
| const handleKillAgents = useCallback(() => { |
| const tasks = store.getState().tasks |
| const hasRunningAgents = Object.values(tasks).some( |
| t => t.type === 'local_agent' && t.status === 'running', |
| ) |
| if (!hasRunningAgents) { |
| addNotification({ |
| key: 'kill-agents-none', |
| text: 'No background agents running', |
| priority: 'immediate', |
| timeoutMs: 2000, |
| }) |
| return |
| } |
| const now = Date.now() |
| const elapsed = now - lastKillAgentsPressRef.current |
| if (elapsed <= KILL_AGENTS_CONFIRM_WINDOW_MS) { |
| |
| lastKillAgentsPressRef.current = 0 |
| removeNotification('kill-agents-confirm') |
| logEvent('tengu_cancel', { |
| source: |
| 'kill_agents' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| clearCommandQueue() |
| killAllAgentsAndNotify() |
| return |
| } |
| |
| lastKillAgentsPressRef.current = now |
| const shortcut = getShortcutDisplay( |
| 'chat:killAgents', |
| 'Chat', |
| 'ctrl+x ctrl+k', |
| ) |
| addNotification({ |
| key: 'kill-agents-confirm', |
| text: `Press ${shortcut} again to stop background agents`, |
| priority: 'immediate', |
| timeoutMs: KILL_AGENTS_CONFIRM_WINDOW_MS, |
| }) |
| }, [store, addNotification, removeNotification, killAllAgentsAndNotify]) |
|
|
| |
| |
| |
| useKeybinding('chat:killAgents', handleKillAgents, { |
| context: 'Chat', |
| }) |
|
|
| return null |
| } |
|
|