@@ -23,6 +23,7 @@ import { useChatStore } from './state/chat-store'
2323import { createChatScrollAcceleration } from './utils/chat-scroll-accel'
2424import { formatQueuedPreview } from './utils/helpers'
2525import { loadLocalAgents } from './utils/local-agent-registry'
26+ import { flushAnalytics } from './utils/analytics'
2627import { logger } from './utils/logger'
2728import { buildMessageTree } from './utils/message-tree-utils'
2829import { 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 )}
0 commit comments