Skip to content

Commit b74e98b

Browse files
committed
Ensure Ctrl+C exit prints and flushes marker
1 parent 38d013f commit b74e98b

File tree

3 files changed

+59
-18
lines changed

3 files changed

+59
-18
lines changed

cli/src/hooks/use-exit-handler.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
22

33
import { getCurrentChatId } from '../project-files'
44
import { flushAnalytics } from '../utils/analytics'
5+
import { scheduleGracefulExit } from '../utils/graceful-exit'
56

67
import type { InputValue } from '../state/chat-store'
78

@@ -66,18 +67,7 @@ export const useExitHandler = ({
6667
exitFallbackTimeoutRef.current = null
6768
}
6869

69-
try {
70-
process.stdout.write('\nGoodbye! Exiting...\n')
71-
// Ensure a clear exit marker is rendered for terminal snapshots
72-
process.stdout.write('exit\n')
73-
} catch {
74-
// Ignore stdout write errors during shutdown
75-
}
76-
77-
// Give the terminal a moment to render the exit message before terminating
78-
setTimeout(() => {
79-
process.exit(0)
80-
}, 25)
70+
scheduleGracefulExit()
8171
}, [])
8272

8373
const flushAnalyticsWithTimeout = useCallback(async (timeoutMs = 1000) => {

cli/src/index.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { initializeApp } from './init/init-app'
2222
import { getProjectRoot } from './project-files'
2323
import { initAnalytics } from './utils/analytics'
2424
import { getUserCredentials } from './utils/auth'
25+
import { scheduleGracefulExit } from './utils/graceful-exit'
2526
import { clearLogFile, logger } from './utils/logger'
2627
import { detectTerminalTheme } from './utils/terminal-color-detection'
2728
import { setOscDetectedTheme } from './utils/theme-system'
@@ -33,12 +34,7 @@ let globalSigintHandled = false
3334
process.on('SIGINT', () => {
3435
if (globalSigintHandled) return
3536
globalSigintHandled = true
36-
try {
37-
process.stdout.write('\nGoodbye! Exiting (SIGINT)...\nexit\n')
38-
} catch {
39-
// Ignore write errors during shutdown
40-
}
41-
process.exit(0)
37+
scheduleGracefulExit({ message: '\nGoodbye! Exiting (SIGINT)...\nexit\n' })
4238
})
4339

4440
const require = createRequire(import.meta.url)

cli/src/utils/graceful-exit.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const EXIT_MESSAGE = '\nGoodbye! Exiting...\nexit\n'
2+
3+
let exitStarted = false
4+
5+
function sleep(ms: number): Promise<void> {
6+
return new Promise((resolve) => setTimeout(resolve, ms))
7+
}
8+
9+
async function flushExitMessage(message: string): Promise<void> {
10+
await new Promise<void>((resolve) => {
11+
const handleDrain = () => resolve()
12+
const flushed = process.stdout.write(message, handleDrain)
13+
if (!flushed) {
14+
process.stdout.once('drain', handleDrain)
15+
}
16+
17+
// Always resolve eventually in case stdout is interrupted
18+
setTimeout(resolve, 80)
19+
})
20+
}
21+
22+
/**
23+
* Ensure we print a visible exit marker and give stdout a chance to flush
24+
* before forcing the process to exit.
25+
*/
26+
export async function gracefulExit(options?: {
27+
message?: string
28+
code?: number
29+
}): Promise<void> {
30+
if (exitStarted) return
31+
exitStarted = true
32+
33+
const message = options?.message ?? EXIT_MESSAGE
34+
const code = options?.code ?? 0
35+
36+
try {
37+
await flushExitMessage(message)
38+
// Small delay to let terminal emulators render the exit marker
39+
await sleep(30)
40+
} catch {
41+
// Ignore errors and fall through to exit
42+
}
43+
44+
process.exit(code)
45+
}
46+
47+
/**
48+
* Fire-and-forget exit helper that still flushes stdout before exiting.
49+
*/
50+
export function scheduleGracefulExit(options?: {
51+
message?: string
52+
code?: number
53+
}): void {
54+
void gracefulExit(options)
55+
}

0 commit comments

Comments
 (0)