import { c as _c } from "react-compiler-runtime";
import chalk from 'chalk';
import type { UUID } from 'crypto';
import figures from 'figures';
import * as React from 'react';
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js';
import { buildGoalStartInstruction } from '../../services/goal/instructions.js';
import { saveGoalState } from '../../services/goal/persistence.js';
import { resumeGoal } from '../../services/goal/state.js';
import type { GoalState } from '../../services/goal/types.js';
import type { CommandResultDisplay, ResumeEntrypoint } from '../../commands.js';
import { LogSelector } from '../../components/LogSelector.js';
import { MessageResponse } from '../../components/MessageResponse.js';
import { SessionSummary } from '../../components/SessionSummary.js';
import { Spinner } from '../../components/Spinner.js';
import { useIsInsideModal } from '../../context/modalContext.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { setClipboard } from '../../ink/termio/osc.js';
import { Box, Text, useInput } from '../../ink.js';
import type { LocalJSXCommandCall } from '../../types/command.js';
import type { LogOption } from '../../types/logs.js';
import type { TodoItem, TodoList } from '../../utils/todo/types.js';
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js';
import { checkCrossProjectResume } from '../../utils/crossProjectResume.js';
import { getWorktreePaths } from '../../utils/getWorktreePaths.js';
import { logError } from '../../utils/log.js';
import { loadReplayIndex } from '../../utils/replayIndex.js';
import { getLastSessionLog, getSessionIdFromLog, getTranscriptPathForSession, isCustomTitleEnabled, isLiteLog, loadAllProjectsMessageLogs, loadFullLog, loadSameRepoMessageLogs, searchSessionsByCustomTitle } from '../../utils/sessionStorage.js';
import { validateUuid } from '../../utils/uuid.js';
type ResumeResult = {
resultType: 'sessionNotFound';
arg: string;
} | {
resultType: 'multipleMatches';
arg: string;
count: number;
};
function resumeHelpMessage(result: ResumeResult): string {
switch (result.resultType) {
case 'sessionNotFound':
return `Session ${chalk.bold(result.arg)} was not found.`;
case 'multipleMatches':
return `Found ${result.count} sessions matching ${chalk.bold(result.arg)}. Please use /resume to pick a specific session.`;
}
}
type ResumeConfirmationSession = {
sessionId: UUID;
log: LogOption;
}
export function ResumeConfirmation({
selectedSession,
sessionSummary,
resuming,
onResume,
onCancel,
}: {
selectedSession: ResumeConfirmationSession;
sessionSummary: import('../../types/logs.js').ReplaySummary;
resuming: boolean;
onResume: (session: ResumeConfirmationSession) => void;
onCancel: () => void;
}): React.ReactNode {
useInput((_input, key) => {
if (key.return) {
onResume(selectedSession);
} else if (key.escape) {
onCancel();
}
}, {
isActive: !resuming,
});
return (
Press
Enter
to resume,
Escape
to cancel
);
}
function ResumeError(t0) {
const $ = _c(10);
const {
message,
args,
onDone
} = t0;
let t1;
let t2;
if ($[0] !== onDone) {
t1 = () => {
const timer = setTimeout(onDone, 0);
return () => clearTimeout(timer);
};
t2 = [onDone];
$[0] = onDone;
$[1] = t1;
$[2] = t2;
} else {
t1 = $[1];
t2 = $[2];
}
React.useEffect(t1, t2);
let t3;
if ($[3] !== args) {
t3 = {figures.pointer} /resume {args};
$[3] = args;
$[4] = t3;
} else {
t3 = $[4];
}
let t4;
if ($[5] !== message) {
t4 = {message};
$[5] = message;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== t3 || $[8] !== t4) {
t5 = {t3}{t4};
$[7] = t3;
$[8] = t4;
$[9] = t5;
} else {
t5 = $[9];
}
return t5;
}
function ResumeCommand({
onDone,
onResume
}: {
onDone: (result?: string, options?: {
display?: CommandResultDisplay;
}) => void;
onResume: (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => Promise;
}): React.ReactNode {
const [logs, setLogs] = React.useState([]);
const [worktreePaths, setWorktreePaths] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [resuming, setResuming] = React.useState(false);
const [showAllProjects, setShowAllProjects] = React.useState(false);
const [selectedSession, setSelectedSession] = React.useState<{ sessionId: UUID; log: LogOption } | null>(null);
const [sessionSummary, setSessionSummary] = React.useState(null);
const {
rows
} = useTerminalSize();
const insideModal = useIsInsideModal();
const loadLogs = React.useCallback(async (allProjects: boolean, paths: string[]) => {
setLoading(true);
try {
const allLogs = allProjects ? await loadAllProjectsMessageLogs() : await loadSameRepoMessageLogs(paths);
const resumable = filterResumableSessions(allLogs, getSessionId());
if (resumable.length === 0) {
onDone('No conversations found to resume');
return;
}
setLogs(resumable);
} catch (_err) {
onDone('Failed to load conversations');
} finally {
setLoading(false);
}
}, [onDone]);
React.useEffect(() => {
async function init() {
const paths_0 = await getWorktreePaths(getOriginalCwd());
setWorktreePaths(paths_0);
void loadLogs(false, paths_0);
}
void init();
}, [loadLogs]);
const handleToggleAllProjects = React.useCallback(() => {
const newValue = !showAllProjects;
setShowAllProjects(newValue);
void loadLogs(newValue, worktreePaths);
}, [showAllProjects, loadLogs, worktreePaths]);
async function handleSelect(log: LogOption) {
const sessionId = validateUuid(getSessionIdFromLog(log));
if (!sessionId) {
onDone('Failed to resume conversation');
return;
}
// Load full messages for lite logs
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
// Check if this conversation is from a different directory
const crossProjectCheck = checkCrossProjectResume(fullLog, showAllProjects, worktreePaths);
if (crossProjectCheck.isCrossProject) {
if (crossProjectCheck.isSameRepoWorktree) {
// Same repo worktree - can resume directly
// Try to load replay summary
const transcriptPath = fullLog.fullPath || getTranscriptPathForSession(sessionId);
const replayIndex = await loadReplayIndex(sessionId, transcriptPath);
if (replayIndex) {
setSessionSummary(replayIndex.summary);
setSelectedSession({ sessionId, log: fullLog });
} else {
setResuming(true);
void onResume(sessionId, fullLog, 'slash_command_picker');
}
return;
}
// Different project - show command instead of resuming
const raw = await setClipboard(crossProjectCheck.command);
if (raw) process.stdout.write(raw);
// Format the output message
const message = ['', 'This conversation is from a different directory.', '', 'To resume, run:', ` ${crossProjectCheck.command}`, '', '(Command copied to clipboard)', ''].join('\n');
onDone(message, {
display: 'user'
});
return;
}
// Same directory - try to show summary first
const transcriptPath = fullLog.fullPath || getTranscriptPathForSession(sessionId);
const replayIndex = await loadReplayIndex(sessionId, transcriptPath);
if (replayIndex) {
setSessionSummary(replayIndex.summary);
setSelectedSession({ sessionId, log: fullLog });
} else {
// No replay data, proceed directly
setResuming(true);
void onResume(sessionId, fullLog, 'slash_command_picker');
}
}
function handleConfirmResume(session = selectedSession) {
if (session) {
setResuming(true);
void onResume(session.sessionId, session.log, 'slash_command_picker');
}
}
function handleCancelResume() {
setSelectedSession(null);
setSessionSummary(null);
}
function handleCancel() {
onDone('Resume cancelled', {
display: 'system'
});
}
if (loading) {
return
Loading conversations…
;
}
if (resuming) {
return
Resuming conversation…
;
}
// Show session summary confirmation if a session is selected
if (selectedSession && sessionSummary) {
return ;
}
return loadLogs(showAllProjects, worktreePaths)} showAllProjects={showAllProjects} onToggleAllProjects={handleToggleAllProjects} onAgenticSearch={agenticSessionSearch} />;
}
export function filterResumableSessions(logs: LogOption[], currentSessionId: string): LogOption[] {
return logs.filter(l => !l.isSidechain && getSessionIdFromLog(l) !== currentSessionId);
}
function DirectResumeConfirmation({
selectedSession,
sessionSummary,
entrypoint,
onResume,
onDone,
}: {
selectedSession: ResumeConfirmationSession;
sessionSummary: import('../../types/logs.js').ReplaySummary;
entrypoint: ResumeEntrypoint;
onResume: (
sessionId: UUID,
log: LogOption,
entrypoint: ResumeEntrypoint,
) => Promise;
onDone: Parameters[0];
}): React.ReactNode {
const [resuming, setResuming] = React.useState(false);
return (
{
if (resuming) return;
setResuming(true);
void onResume(session.sessionId, session.log, entrypoint);
}}
onCancel={() => {
if (resuming) return;
onDone('Resume cancelled', {
display: 'system',
});
}}
/>
);
}
async function resumeWithOptionalSummary(
sessionId: UUID,
log: LogOption,
entrypoint: ResumeEntrypoint,
onResume: (
sessionId: UUID,
log: LogOption,
entrypoint: ResumeEntrypoint,
) => Promise,
onDone: Parameters[0],
): Promise {
const transcriptPath = log.fullPath || getTranscriptPathForSession(sessionId);
const replayIndex = await loadReplayIndex(sessionId, transcriptPath);
if (!replayIndex) {
void onResume(sessionId, log, entrypoint);
return null;
}
return (
);
}
function isResumableGoal(goal: GoalState | null): goal is GoalState {
return goal != null && (goal.status === 'active' || goal.status === 'paused');
}
function formatTodoStatus(status: TodoItem['status']): string {
switch (status) {
case 'in_progress':
return 'in progress';
case 'completed':
return 'done';
default:
return status;
}
}
function formatTodosMessage(todos: TodoList): string {
const lines = todos.map(todo => `- [${formatTodoStatus(todo.status)}] ${todo.content}`);
return lines.length === 0
? 'No todos are currently tracked.'
: ['Current todos:', ...lines].join('\n');
}
const CONTINUE_INSTRUCTION = `The user asked you to continue.
Resume the most recent task based on the conversation transcript. Do not simply acknowledge the request. Pick up exactly where you left off and take the next concrete step. Only ask for clarification if the next action is genuinely ambiguous.`;
function buildGenericContinueMessage(extraContext: string | null): string {
return extraContext
? `${extraContext}\n\n${CONTINUE_INSTRUCTION}`
: CONTINUE_INSTRUCTION;
}
function formatContinuationHint(hint: string | null): string | null {
return hint ? `User continuation hint:\n${hint}` : null;
}
function appendContinuationHint(message: string, hint: string | null): string {
const formattedHint = formatContinuationHint(hint);
return formattedHint ? `${message}\n\n${formattedHint}` : message;
}
async function tryContinueCurrentTask(
context: Parameters[1],
onDone: Parameters[0],
continuationHint: string | null = null,
): Promise {
const appState = context.getAppState();
const currentGoal = appState.goal ?? null;
if (isResumableGoal(currentGoal)) {
const goal =
currentGoal.status === 'paused' ? resumeGoal(currentGoal) : currentGoal;
if (goal !== currentGoal) {
context.setAppState(prev => ({ ...prev, goal }));
try {
await saveGoalState(goal);
} catch (error) {
logError(error as Error);
}
}
onDone(
currentGoal.status === 'active'
? 'Goal already active; continuing.'
: 'Goal resumed.',
{
shouldQuery: true,
metaMessages: [
appendContinuationHint(
buildGoalStartInstruction(goal),
continuationHint,
),
],
},
);
return true;
}
const sessionId = context.agentId ?? getSessionId();
const todos = appState.todos[sessionId];
if (todos && todos.length > 0) {
const todoContext = [
formatTodosMessage(todos),
formatContinuationHint(continuationHint),
].filter(Boolean).join('\n\n');
onDone('Continuing current task.', {
shouldQuery: true,
metaMessages: [buildGenericContinueMessage(todoContext)],
});
return true;
}
return false;
}
export const continueCall: LocalJSXCommandCall = async (onDone, context, args) => {
const arg = args?.trim() || null;
if (await tryContinueCurrentTask(context, onDone, arg)) {
return null;
}
onDone('Continuing current task.', {
shouldQuery: true,
metaMessages: [
buildGenericContinueMessage(formatContinuationHint(arg)),
],
});
return null;
};
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
const onResume = async (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => {
try {
await context.resume?.(sessionId, log, entrypoint);
onDone(undefined, {
display: 'skip'
});
} catch (error) {
logError(error as Error);
onDone(`Failed to resume: ${(error as Error).message}`);
}
};
const arg = args?.trim();
// No argument provided - show picker
if (!arg) {
return ;
}
// Load logs to search (includes same-repo worktrees)
const worktreePaths = await getWorktreePaths(getOriginalCwd());
const logs = await loadSameRepoMessageLogs(worktreePaths);
if (logs.length === 0) {
const message = 'No conversations found to resume.';
return onDone(message)} />;
}
// First, check if arg is a valid UUID
const maybeSessionId = validateUuid(arg);
if (maybeSessionId) {
const matchingLogs = logs.filter(l => getSessionIdFromLog(l) === maybeSessionId).sort((a, b) => b.modified.getTime() - a.modified.getTime());
if (matchingLogs.length > 0) {
const log = matchingLogs[0]!;
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
return resumeWithOptionalSummary(
maybeSessionId,
fullLog,
'slash_command_session_id',
onResume,
onDone,
);
}
// Enriched logs didn't find it — try direct file lookup. This handles
// sessions filtered out by enrichLogs (e.g., first message >16KB makes
// firstPrompt extraction fail, causing the session to be dropped).
const directLog = await getLastSessionLog(maybeSessionId);
if (directLog) {
return resumeWithOptionalSummary(
maybeSessionId,
directLog,
'slash_command_session_id',
onResume,
onDone,
);
}
}
// Next, try exact custom title match (only if feature is enabled)
if (isCustomTitleEnabled()) {
const titleMatches = await searchSessionsByCustomTitle(arg, {
exact: true
});
if (titleMatches.length === 1) {
const log = titleMatches[0]!;
const sessionId = getSessionIdFromLog(log);
if (sessionId) {
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
return resumeWithOptionalSummary(
sessionId,
fullLog,
'slash_command_title',
onResume,
onDone,
);
}
}
// Multiple matches - show error
if (titleMatches.length > 1) {
const message = resumeHelpMessage({
resultType: 'multipleMatches',
arg,
count: titleMatches.length
});
return onDone(message)} />;
}
}
// No match found - show error
const message = resumeHelpMessage({
resultType: 'sessionNotFound',
arg
});
return onDone(message)} />;
};