@@ -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