Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions src/screens/ResumeConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Spinner } from '../components/Spinner.js';
import { restoreCostStateForSession } from '../cost-tracker.js';
import { setClipboard } from '../ink/termio/osc.js';
import { Box, Text } from '../ink.js';
import { AlternateScreen } from '../ink/components/AlternateScreen.js';
import useApp from '../ink/hooks/use-app.js';
import { useKeybinding } from '../keybindings/useKeybinding.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics/index.js';
import type { MCPServerConnection, ScopedMcpServerConfig } from '../services/mcp/types.js';
Expand All @@ -27,6 +29,7 @@ import { loadConversationForResume } from '../utils/conversationRecovery.js';
import { checkCrossProjectResume } from '../utils/crossProjectResume.js';
import { errorMessage } from '../utils/errors.js';
import type { FileHistorySnapshot } from '../utils/fileHistory.js';
import { isMouseTrackingEnabled, isTmuxControlMode } from '../utils/fullscreen.js';
import { logError } from '../utils/log.js';
import { createSystemMessage } from '../utils/messages.js';
import { computeStandaloneAgentContext, createForkSessionInfoMessage, restoreAgentFromSession, restoreWorktreeForResume } from '../utils/sessionRestore.js';
Expand Down Expand Up @@ -87,6 +90,7 @@ export function ResumeConversation({
} = useTerminalSize();
const agentDefinitions = useAppState(s => s.agentDefinitions);
const setAppState = useSetAppState();
const { exit } = useApp();
const [logs, setLogs] = React.useState<LogOption[]>([]);
const [loading, setLoading] = React.useState(true);
const [resuming, setResuming] = React.useState(false);
Expand Down Expand Up @@ -157,9 +161,34 @@ export function ResumeConversation({
setShowAllProjects(newValue);
loadLogs(newValue);
}, [showAllProjects, loadLogs]);
/**
* Exit the picker with the given code after Ink has unmounted.
* Unmount restores terminal modes and exits the alternate screen back to
* the main buffer via synchronous writes; any remaining async output is
* flushed first via the drain callback because process.exit() drops
* unflushed writes. The failsafe covers a stdout that never drains.
*/
function exitPicker(exitCode: number): void {
try {
process.stdout.write('', () => {
// eslint-disable-next-line custom-rules/no-process-exit -- flush-then-exit, see above
process.exit(exitCode);
});
} catch {
// eslint-disable-next-line custom-rules/no-process-exit -- flush-then-exit, see above
process.exit(exitCode);
}
// eslint-disable-next-line custom-rules/no-process-exit -- failsafe, see above
setTimeout(() => process.exit(exitCode), 500);
}
function onCancel() {
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1);
// Unmount first so terminal modes are restored and the alternate screen
// is exited back to the main buffer; exitPicker flushes remaining output
// before exiting. Exiting here preempts renderAndRun's shutdown epilogue,
// so no resume hint or SessionEnd hooks run for a cancel — same observable
// behavior as before, minus the residue. Exit code 1 (cancel) is preserved.
exit();
exitPicker(1);
}
async function onSelect(log_0: LogOption) {
setResumeError(null);
Expand Down Expand Up @@ -310,10 +339,19 @@ export function ResumeConversation({
if (filteredLogs.length === 0) {
return <NoConversationsMessage />;
}
return <Box flexDirection="column">
const picker = <Box flexDirection="column">
{resumeErrorBanner}
<LogSelector logs={filteredLogs} maxHeight={rows} onCancel={onCancel} onSelect={onSelect} onLogsChanged={isResumeWithRenameEnabled ? () => loadLogs(showAllProjects) : undefined} onLoadMore={loadMoreLogs} initialSearchQuery={initialSearchQuery} showAllProjects={showAllProjects} onToggleAllProjects={handleToggleAllProjects} onAgenticSearch={agenticSessionSearch} />
</Box>;
// Run the picker on the alternate screen everywhere except tmux -CC, where
// alt-screen is known to corrupt terminal state. Cancelling then restores
// the main buffer instead of leaving a stale frame: Ink deliberately
// persists the last inline frame on exit, so inline rendering can never
// exit cleanly here. Mirrors the REPL's <AlternateScreen> usage.
if (!isTmuxControlMode()) {
return <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>{picker}</AlternateScreen>;
}
return picker;
}
function NoConversationsMessage() {
const $ = _c(2);
Expand Down
Loading