Add exit reason types and improve graceful shutdown handling
This commit is contained in:
@@ -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 = {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user