Add exit reason types and improve graceful shutdown handling

This commit is contained in:
Raj Rasane
2026-04-02 14:00:32 +05:30
parent 1059915c84
commit 7f969200fb
3 changed files with 21 additions and 6 deletions

View File

@@ -441,3 +441,8 @@ export async function connectRemoteControl(
): Promise<RemoteControlHandle | null> { ): Promise<RemoteControlHandle | null> {
throw new Error('not implemented') throw new Error('not implemented')
} }
// add exit reason types for removing the error within gracefulShutdown file
export type ExitReason = {
}

View File

@@ -137,7 +137,7 @@ import { generateSessionTitle } from '../utils/sessionTitle.js';
import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, LOCAL_COMMAND_STDOUT_TAG } from '../constants/xml.js'; import { BASH_INPUT_TAG, COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG, LOCAL_COMMAND_STDOUT_TAG } from '../constants/xml.js';
import { escapeXml } from '../utils/xml.js'; import { escapeXml } from '../utils/xml.js';
import type { ThinkingConfig } from '../utils/thinking.js'; import type { ThinkingConfig } from '../utils/thinking.js';
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'; import { gracefulShutdownSync, isShuttingDown } from '../utils/gracefulShutdown.js';
import { handlePromptSubmit, type PromptInputHelpers } from '../utils/handlePromptSubmit.js'; import { handlePromptSubmit, type PromptInputHelpers } from '../utils/handlePromptSubmit.js';
import { useQueueProcessor } from '../hooks/useQueueProcessor.js'; import { useQueueProcessor } from '../hooks/useQueueProcessor.js';
import { useMailboxBridge } from '../hooks/useMailboxBridge.js'; import { useMailboxBridge } from '../hooks/useMailboxBridge.js';
@@ -4886,7 +4886,7 @@ export function REPL({
{mrRender()} {mrRender()}
{!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && <> {!toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && !isShuttingDown() && <>
{autoRunIssueReason && <AutoRunIssueNotification onRun={handleAutoRunIssue} onCancel={handleCancelAutoRunIssue} reason={getAutoRunIssueReasonText(autoRunIssueReason)} />} {autoRunIssueReason && <AutoRunIssueNotification onRun={handleAutoRunIssue} onCancel={handleCancelAutoRunIssue} reason={getAutoRunIssueReasonText(autoRunIssueReason)} />}
{postCompactSurvey.state !== 'closed' ? <FeedbackSurvey state={postCompactSurvey.state} lastResponse={postCompactSurvey.lastResponse} handleSelect={postCompactSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} /> : memorySurvey.state !== 'closed' ? <FeedbackSurvey state={memorySurvey.state} lastResponse={memorySurvey.lastResponse} handleSelect={memorySurvey.handleSelect} handleTranscriptSelect={memorySurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} message="How well did Claude use its memory? (optional)" /> : <FeedbackSurvey state={feedbackSurvey.state} lastResponse={feedbackSurvey.lastResponse} handleSelect={feedbackSurvey.handleSelect} handleTranscriptSelect={feedbackSurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback} />} {postCompactSurvey.state !== 'closed' ? <FeedbackSurvey state={postCompactSurvey.state} lastResponse={postCompactSurvey.lastResponse} handleSelect={postCompactSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} /> : memorySurvey.state !== 'closed' ? <FeedbackSurvey state={memorySurvey.state} lastResponse={memorySurvey.lastResponse} handleSelect={memorySurvey.handleSelect} handleTranscriptSelect={memorySurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={handleSurveyRequestFeedback} message="How well did Claude use its memory? (optional)" /> : <FeedbackSurvey state={feedbackSurvey.state} lastResponse={feedbackSurvey.lastResponse} handleSelect={feedbackSurvey.handleSelect} handleTranscriptSelect={feedbackSurvey.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} onRequestFeedback={didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback} />}
{/* Frustration-triggered transcript sharing prompt */} {/* Frustration-triggered transcript sharing prompt */}

View File

@@ -56,7 +56,7 @@ import { profileReport } from './startupProfiler.js'
* 3. Failing to disable leaves the terminal in a broken state * 3. Failing to disable leaves the terminal in a broken state
*/ */
/* eslint-disable custom-rules/no-sync-fs -- must be sync to flush before process.exit */ /* eslint-disable custom-rules/no-sync-fs -- must be sync to flush before process.exit */
function cleanupTerminalModes(): void { function cleanupTerminalModes(skipUnmount: boolean = false): void {
if (!process.stdout.isTTY) { if (!process.stdout.isTTY) {
return return
} }
@@ -84,7 +84,7 @@ function cleanupTerminalModes(): void {
// Calling unmount() now does the final render on the alt buffer, // Calling unmount() now does the final render on the alt buffer,
// unsubscribes from signal-exit, and writes 1049l exactly once. // unsubscribes from signal-exit, and writes 1049l exactly once.
const inst = instances.get(process.stdout) const inst = instances.get(process.stdout)
if (inst?.isAltScreenActive) { if (!skipUnmount && inst?.isAltScreenActive) {
try { try {
inst.unmount() inst.unmount()
} catch { } catch {
@@ -92,6 +92,11 @@ function cleanupTerminalModes(): void {
// so printResumeHint still hits the main buffer. // so printResumeHint still hits the main buffer.
writeSync(1, EXIT_ALT_SCREEN) writeSync(1, EXIT_ALT_SCREEN)
} }
} else if (skipUnmount && inst?.isAltScreenActive) {
// We already unmounted asynchronously in gracefulShutdown, but if we
// fallback to manual alt-screen exit here just in case Ink didn't write it or is dead.
// Actually, AlternateScreen unmount writes EXIT_ALT_SCREEN, so if we awaited unmount,
// we shouldn't emit it again. So we just do nothing here.
} }
// Catches events that arrived during the unmount tree-walk. // Catches events that arrived during the unmount tree-walk.
// detachForShutdown() below also drains. // detachForShutdown() below also drains.
@@ -411,12 +416,17 @@ export async function gracefulShutdown(
) )
const sessionEndTimeoutMs = getSessionEndHookTimeoutMs() const sessionEndTimeoutMs = getSessionEndHookTimeoutMs()
// Await one tick so React can flush pending updates from commands (e.g. hiding
// the autocomplete menu on /exit) before we detach Ink. This lets log-update
// erase floating UI elements from the terminal so they don't linger after exit.
await new Promise(r => setTimeout(r, 20))
// Failsafe: guarantee process exits even if cleanup hangs (e.g., MCP connections). // Failsafe: guarantee process exits even if cleanup hangs (e.g., MCP connections).
// Runs cleanupTerminalModes first so a hung cleanup doesn't leave the terminal dirty. // Runs cleanupTerminalModes first so a hung cleanup doesn't leave the terminal dirty.
// Budget = max(5s, hook budget + 3.5s headroom for cleanup + analytics flush). // Budget = max(5s, hook budget + 3.5s headroom for cleanup + analytics flush).
failsafeTimer = setTimeout( failsafeTimer = setTimeout(
code => { code => {
cleanupTerminalModes() cleanupTerminalModes(true)
printResumeHint() printResumeHint()
forceExit(code) forceExit(code)
}, },
@@ -433,7 +443,7 @@ export async function gracefulShutdown(
// cleanup (e.g., SIGKILL during macOS reboot). Without this, the resume // cleanup (e.g., SIGKILL during macOS reboot). Without this, the resume
// hint would only appear after cleanup functions, hooks, and analytics // hint would only appear after cleanup functions, hooks, and analytics
// flush — which can take several seconds. // flush — which can take several seconds.
cleanupTerminalModes() cleanupTerminalModes(true)
printResumeHint() printResumeHint()
// Flush session data first — this is the most critical cleanup. If the // Flush session data first — this is the most critical cleanup. If the