Skip to content

Commit 77d1d3d

Browse files
committed
Handle Ctrl+C with exit warning
1 parent ea83737 commit 77d1d3d

File tree

2 files changed

+69
-11
lines changed

2 files changed

+69
-11
lines changed

cli/src/chat.tsx

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useChatStore } from './state/chat-store'
2323
import { createChatScrollAcceleration } from './utils/chat-scroll-accel'
2424
import { formatQueuedPreview } from './utils/helpers'
2525
import { loadLocalAgents } from './utils/local-agent-registry'
26+
import { flushAnalytics } from './utils/analytics'
2627
import { logger } from './utils/logger'
2728
import { buildMessageTree } from './utils/message-tree-utils'
2829
import { chatThemes, createMarkdownPalette } from './utils/theme-system'
@@ -92,15 +93,19 @@ export const App = ({
9293
hasInvalidCredentials: boolean | null
9394
}) => {
9495
const renderer = useRenderer()
95-
const { width: terminalWidth } = useTerminalDimensions()
96+
const { width: measuredWidth } = useTerminalDimensions()
9697
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
9798
const inputRef = useRef<InputRenderable | null>(null)
99+
const terminalWidth = measuredWidth || renderer?.width || 80
98100
const separatorWidth = Math.max(0, terminalWidth - 2)
99101

100102
const themeName = useSystemThemeDetector()
101103
const theme = chatThemes[themeName]
102104
const markdownPalette = useMemo(() => createMarkdownPalette(theme), [theme])
103105

106+
const [exitWarning, setExitWarning] = useState<string | null>(null)
107+
const exitArmedRef = useRef(false)
108+
104109
// Track authentication state
105110
const [isAuthenticated, setIsAuthenticated] = useState(!requireAuth)
106111
const [user, setUser] = useState<User | null>(null)
@@ -203,6 +208,13 @@ export const App = ({
203208
renderer?.setBackgroundColor(theme.background)
204209
}, [renderer, theme.background])
205210

211+
useEffect(() => {
212+
if (exitArmedRef.current && inputValue.length > 0) {
213+
exitArmedRef.current = false
214+
setExitWarning(null)
215+
}
216+
}, [inputValue])
217+
206218
const abortControllerRef = useRef<AbortController | null>(null)
207219

208220
const registerAgentRef = useCallback((agentId: string, element: any) => {
@@ -227,6 +239,28 @@ export const App = ({
227239

228240
const localAgents = useMemo(() => loadLocalAgents(), [])
229241

242+
const handleCtrlC = useCallback(() => {
243+
if (exitArmedRef.current) {
244+
exitArmedRef.current = false
245+
setExitWarning(null)
246+
247+
const flushed = flushAnalytics()
248+
if (flushed && typeof (flushed as Promise<void>).finally === 'function') {
249+
;(flushed as Promise<void>).finally(() => process.exit(0))
250+
} else {
251+
process.exit(0)
252+
}
253+
return true
254+
}
255+
256+
exitArmedRef.current = true
257+
setExitWarning('Press Ctrl+C again to exit')
258+
setInputValue('')
259+
setInputFocused(true)
260+
inputRef.current?.focus()
261+
return true
262+
}, [flushAnalytics, setExitWarning, setInputFocused, setInputValue])
263+
230264
const {
231265
slashContext,
232266
mentionContext,
@@ -596,6 +630,7 @@ export const App = ({
596630
navigateUp,
597631
navigateDown,
598632
toggleAgentMode,
633+
onCtrlC: handleCtrlC,
599634
})
600635

601636
const { tree: messageTree, topLevelMessages } = useMemo(
@@ -646,6 +681,16 @@ export const App = ({
646681
</text>
647682
) : null
648683

684+
const shouldShowQueuePreview = queuedMessages.length > 0
685+
const shouldShowStatusLine = Boolean(exitWarning || hasStatus || shouldShowQueuePreview)
686+
const statusIndicatorNode = (
687+
<StatusIndicator
688+
isProcessing={isWaitingForResponse}
689+
theme={theme}
690+
clipboardMessage={clipboardMessage}
691+
/>
692+
)
693+
649694
// Show login screen if not authenticated
650695
if (!isAuthenticated) {
651696
return (
@@ -723,7 +768,7 @@ export const App = ({
723768
backgroundColor: theme.panelBg,
724769
}}
725770
>
726-
{(hasStatus || queuedMessages.length > 0) && (
771+
{shouldShowStatusLine && (
727772
<box
728773
style={{
729774
flexDirection: 'row',
@@ -732,21 +777,21 @@ export const App = ({
732777
}}
733778
>
734779
<text wrap={false}>
735-
<StatusIndicator
736-
isProcessing={isWaitingForResponse}
737-
theme={theme}
738-
clipboardMessage={clipboardMessage}
739-
/>
740-
{hasStatus && queuedMessages.length > 0 && ' '}
741-
{queuedMessages.length > 0 && (
780+
{hasStatus ? statusIndicatorNode : null}
781+
{hasStatus && (exitWarning || shouldShowQueuePreview) ? ' ' : ''}
782+
{exitWarning ? (
783+
<span fg={theme.statusSecondary}>{exitWarning}</span>
784+
) : null}
785+
{exitWarning && shouldShowQueuePreview ? ' ' : ''}
786+
{shouldShowQueuePreview ? (
742787
<span fg={theme.statusSecondary} bg={theme.inputFocusedBg}>
743788
{' '}
744789
{formatQueuedPreview(
745790
queuedMessages,
746791
Math.max(30, terminalWidth - 25),
747792
)}{' '}
748793
</span>
749-
)}
794+
) : null}
750795
</text>
751796
</box>
752797
)}

cli/src/hooks/use-keyboard-handlers.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface KeyboardHandlersConfig {
1515
navigateUp: () => void
1616
navigateDown: () => void
1717
toggleAgentMode: () => void
18+
onCtrlC: () => boolean
1819
}
1920

2021
export const useKeyboardHandlers = ({
@@ -29,6 +30,7 @@ export const useKeyboardHandlers = ({
2930
navigateUp,
3031
navigateDown,
3132
toggleAgentMode,
33+
onCtrlC,
3234
}: KeyboardHandlersConfig) => {
3335
useKeyboard(
3436
useCallback(
@@ -48,8 +50,19 @@ export const useKeyboardHandlers = ({
4850
abortControllerRef.current.abort()
4951
}
5052
}
53+
54+
if (isCtrlC) {
55+
const shouldPrevent = onCtrlC()
56+
if (
57+
shouldPrevent &&
58+
'preventDefault' in key &&
59+
typeof key.preventDefault === 'function'
60+
) {
61+
key.preventDefault()
62+
}
63+
}
5164
},
52-
[isStreaming, isWaitingForResponse, abortControllerRef],
65+
[isStreaming, isWaitingForResponse, abortControllerRef, onCtrlC],
5366
),
5467
)
5568

0 commit comments

Comments
 (0)