diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index 8c3cd9900c..b03ae3e8f5 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -26,7 +26,9 @@ import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { ExtensionManager } from './extension-manager.js';
-import { RESUME_LATEST } from '../utils/sessionUtils.js';
+import {
+ RESUME_LAST_IN_CURRENT_FOLDER,
+} from '../utils/sessionUtils.js';
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi.fn(() => ({ isTrusted: true, source: 'file' })), // Default to trusted
@@ -548,14 +550,27 @@ describe('parseArguments', () => {
}
});
- it('should return RESUME_LATEST constant when --resume is passed without a value', async () => {
+ it('should return RESUME_LAST_IN_CURRENT_FOLDER when --resume is passed without a value', async () => {
const originalIsTTY = process.stdin.isTTY;
process.stdin.isTTY = true; // Make it interactive to avoid validation error
process.argv = ['node', 'script.js', '--resume'];
try {
const argv = await parseArguments(createTestMergedSettings());
- expect(argv.resume).toBe(RESUME_LATEST);
+ expect(argv.resume).toBe(RESUME_LAST_IN_CURRENT_FOLDER);
+ expect(argv.resume).toBe('__last_in_current_folder__');
+ } finally {
+ process.stdin.isTTY = originalIsTTY;
+ }
+ });
+
+ it('should keep explicit --resume latest unchanged', async () => {
+ const originalIsTTY = process.stdin.isTTY;
+ process.stdin.isTTY = true;
+ process.argv = ['node', 'script.js', '--resume', 'latest'];
+
+ try {
+ const argv = await parseArguments(createTestMergedSettings());
expect(argv.resume).toBe('latest');
} finally {
process.stdin.isTTY = originalIsTTY;
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 7bd34929b3..3d8db9daf9 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -53,7 +53,10 @@ import {
import { loadSandboxConfig } from './sandboxConfig.js';
import { resolvePath } from '../utils/resolvePath.js';
-import { RESUME_LATEST } from '../utils/sessionUtils.js';
+import {
+ RESUME_LAST_IN_CURRENT_FOLDER,
+ RESUME_LATEST,
+} from '../utils/sessionUtils.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { createPolicyEngineConfig } from './policy.js';
@@ -81,7 +84,11 @@ export interface CliArgs {
experimentalAcp: boolean | undefined;
extensions: string[] | undefined;
listExtensions: boolean | undefined;
- resume: string | typeof RESUME_LATEST | undefined;
+ resume:
+ | string
+ | typeof RESUME_LATEST
+ | typeof RESUME_LAST_IN_CURRENT_FOLDER
+ | undefined;
listSessions: boolean | undefined;
deleteSession: string | undefined;
includeDirectories: string[] | undefined;
@@ -224,14 +231,14 @@ export async function parseArguments(
// one, and not being passed at all.
skipValidation: true,
description:
- 'Resume a previous session by name, index, UUID, or "latest" (e.g. --resume my-chat-abc12)',
+ 'Resume a previous session by name, index, UUID, or "latest". If passed without a value, resumes the most recent session from the current folder when available.',
coerce: (value: string): string => {
// When --resume passed with a value (`gemini --resume 123`): value = "123" (string)
// When --resume passed without a value (`gemini --resume`): value = "" (string)
// When --resume not passed at all: this `coerce` function is not called at all, and
// `yargsInstance.argv.resume` is undefined.
if (value === '') {
- return RESUME_LATEST;
+ return RESUME_LAST_IN_CURRENT_FOLDER;
}
return value;
},
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 284153ffc0..7b092d2d5a 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -109,6 +109,9 @@ import { runDeferredCommand } from './deferred.js';
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
const SLOW_RENDER_MS = 200;
+type ResumedSessionDataWithNotice = ResumedSessionData & {
+ resumeNotice?: string;
+};
export function validateDnsResolutionOrder(
order: string | undefined,
@@ -677,17 +680,19 @@ export async function main() {
];
// Handle --resume flag
- let resumedSessionData: ResumedSessionData | undefined = undefined;
+ let resumedSessionData: ResumedSessionDataWithNotice | undefined = undefined;
if (argv.resume) {
const sessionSelector = new SessionSelector(config);
try {
const result = await sessionSelector.resolveSession(argv.resume);
- resumedSessionData = {
+ const resolvedSessionData: ResumedSessionDataWithNotice = {
conversation: result.sessionData,
filePath: result.sessionPath,
+ resumeNotice: result.resumeNotice,
};
+ resumedSessionData = resolvedSessionData;
// Use the existing session ID to continue recording to the same session
- config.setSessionId(resumedSessionData.conversation.sessionId);
+ config.setSessionId(resolvedSessionData.conversation.sessionId);
} catch (error) {
coreEvents.emitFeedback(
'error',
diff --git a/packages/cli/src/ui/components/SessionBrowser.test.tsx b/packages/cli/src/ui/components/SessionBrowser.test.tsx
index 2aef319b20..9ead8ab72c 100644
--- a/packages/cli/src/ui/components/SessionBrowser.test.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.test.tsx
@@ -221,6 +221,52 @@ describe('SessionBrowser component', () => {
expect(lastFrame()).toMatchSnapshot();
});
+ it('highlights the latest session in the current folder', () => {
+ const currentFolderLatest = createSession({
+ id: 'folder-latest',
+ sessionName: 'folder-latest-abcde',
+ projectRoot: process.cwd(),
+ lastUpdated: '2025-01-03T10:00:00Z',
+ index: 0,
+ });
+ const currentFolderOlder = createSession({
+ id: 'folder-older',
+ sessionName: 'folder-older-abcde',
+ projectRoot: process.cwd(),
+ lastUpdated: '2025-01-01T10:00:00Z',
+ index: 1,
+ });
+ const otherFolder = createSession({
+ id: 'other-folder',
+ sessionName: 'other-folder-abcde',
+ projectRoot: '/tmp/other-project',
+ lastUpdated: '2025-01-04T10:00:00Z',
+ index: 2,
+ });
+
+ const config = createMockConfig();
+ const onResumeSession = vi.fn();
+ const onDeleteSession = vi.fn().mockResolvedValue(undefined);
+ const onRenameSession = vi.fn();
+ const onExit = vi.fn();
+
+ const { lastFrame } = render(
+ ,
+ );
+
+ expect(lastFrame()).toContain(
+ 'Latest in this folder: folder-latest-abcde',
+ );
+ expect(lastFrame()).toContain('[last in folder]');
+ });
+
it('enters search mode, filters sessions, and renders match snippets', async () => {
const searchSession = createSession({
id: 'search1',
diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx
index a83818ca5f..e7f0ff3dbc 100644
--- a/packages/cli/src/ui/components/SessionBrowser.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.tsx
@@ -88,6 +88,8 @@ export interface SessionBrowserState {
endIndex: number;
/** Sessions visible on current page */
visibleSessions: SessionInfo[];
+ /** Latest session id in the current folder (if any) */
+ lastSessionInCurrentFolderId?: string;
// State setters
/** Update sessions array */
@@ -432,10 +434,35 @@ const isSessionFromCurrentFolder = (projectRoot?: string): boolean => {
}
};
+const parseTimestamp = (timestamp: string): number => {
+ const parsed = new Date(timestamp).getTime();
+ return Number.isNaN(parsed) ? 0 : parsed;
+};
+
+const getLastSessionInCurrentFolderId = (
+ sessions: SessionInfo[],
+): string | undefined => {
+ const currentFolderSessions = sessions.filter((session) =>
+ isSessionFromCurrentFolder(session.projectRoot),
+ );
+ if (currentFolderSessions.length === 0) {
+ return undefined;
+ }
+
+ const latest = currentFolderSessions.reduce((best, candidate) =>
+ parseTimestamp(candidate.lastUpdated) > parseTimestamp(best.lastUpdated)
+ ? candidate
+ : best,
+ );
+ return latest.id;
+};
+
const SessionDetailsPanel = ({
session,
+ lastSessionInCurrentFolderId,
}: {
session: SessionInfo | undefined;
+ lastSessionInCurrentFolderId?: string;
}): React.JSX.Element | null => {
if (!session) {
return null;
@@ -462,10 +489,38 @@ const SessionDetailsPanel = ({
{formatTimestamp(session.lastUpdated)}
+ {session.id === lastSessionInCurrentFolderId && (
+
+ This is your latest session in this folder.
+
+ )}
);
};
+const CurrentFolderSessionHint = ({
+ state,
+}: {
+ state: SessionBrowserState;
+}): React.JSX.Element | null => {
+ if (!state.lastSessionInCurrentFolderId) {
+ return null;
+ }
+
+ const latestCurrentFolderSession = state.sessions.find(
+ (session) => session.id === state.lastSessionInCurrentFolderId,
+ );
+ if (!latestCurrentFolderSession) {
+ return null;
+ }
+
+ return (
+
+ Latest in this folder: {latestCurrentFolderSession.sessionName}
+
+ );
+};
+
/**
* Match snippet display component for search results.
*/
@@ -518,6 +573,8 @@ const SessionItem = ({
state.startIndex + state.visibleSessions.indexOf(session);
const isActive = originalIndex === state.activeIndex;
const isDisabled = session.isCurrentSession;
+ const isLastSessionInCurrentFolder =
+ session.id === state.lastSessionInCurrentFolderId;
const isCurrentFolder = isSessionFromCurrentFolder(session.projectRoot);
const activeColor = isCurrentFolder ? Colors.AccentCyan : Colors.AccentPurple;
const textColor = (c: string = Colors.Foreground) => {
@@ -535,6 +592,9 @@ const SessionItem = ({
if (session.isCurrentSession) {
additionalInfo = ' (current)';
}
+ if (isLastSessionInCurrentFolder) {
+ additionalInfo += ' [last in folder]';
+ }
// Show match snippets if searching and matches exist
if (
@@ -606,7 +666,15 @@ const SessionItem = ({
{truncatedMessage}
{additionalInfo && (
-
+
{additionalInfo}
)}
@@ -686,6 +754,10 @@ export const useSessionBrowserState = (
}, [searchQuery]);
const totalSessions = filteredAndSortedSessions.length;
+ const lastSessionInCurrentFolderId = useMemo(
+ () => getLastSessionInCurrentFolderId(sessions),
+ [sessions],
+ );
const startIndex = scrollOffset;
const endIndex = Math.min(scrollOffset + SESSIONS_PER_PAGE, totalSessions);
const visibleSessions = filteredAndSortedSessions.slice(startIndex, endIndex);
@@ -718,6 +790,7 @@ export const useSessionBrowserState = (
terminalWidth,
filteredAndSortedSessions,
totalSessions,
+ lastSessionInCurrentFolderId,
startIndex,
endIndex,
visibleSessions,
@@ -734,6 +807,8 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
setSessions,
setLoading,
setError,
+ setActiveIndex,
+ setScrollOffset,
isSearchMode,
hasLoadedFullContent,
setHasLoadedFullContent,
@@ -747,6 +822,25 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
config.getProjectRoot(),
);
setSessions(sessionData);
+ const latestCurrentFolderSessionId =
+ getLastSessionInCurrentFolderId(sessionData);
+ if (latestCurrentFolderSessionId) {
+ const sortedByDefault = sortSessions(sessionData, 'date', false);
+ const targetIndex = sortedByDefault.findIndex(
+ (session) => session.id === latestCurrentFolderSessionId,
+ );
+ if (targetIndex >= 0) {
+ setActiveIndex(targetIndex);
+ setScrollOffset(
+ Math.max(
+ 0,
+ targetIndex >= SESSIONS_PER_PAGE
+ ? targetIndex - SESSIONS_PER_PAGE + 1
+ : 0,
+ ),
+ );
+ }
+ }
setLoading(false);
} catch (err) {
setError(
@@ -758,7 +852,14 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
loadSessions();
- }, [config, setSessions, setLoading, setError]);
+ }, [
+ config,
+ setSessions,
+ setLoading,
+ setError,
+ setActiveIndex,
+ setScrollOffset,
+ ]);
useEffect(() => {
const loadFullContent = async () => {
@@ -1056,6 +1157,7 @@ export function SessionBrowserView({
return (
+
{state.isSearchMode && }
{state.isRenameMode && }
@@ -1066,7 +1168,12 @@ export function SessionBrowserView({
)}
- {!state.isSearchMode && }
+ {!state.isSearchMode && (
+
+ )}
);
}
diff --git a/packages/cli/src/ui/hooks/useSessionResume.test.ts b/packages/cli/src/ui/hooks/useSessionResume.test.ts
index 9350cc167a..a3a051ed4d 100644
--- a/packages/cli/src/ui/hooks/useSessionResume.test.ts
+++ b/packages/cli/src/ui/hooks/useSessionResume.test.ts
@@ -16,6 +16,11 @@ import type {
} from '@google/gemini-cli-core';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import type { HistoryItemWithoutId } from '../types.js';
+import { MessageType } from '../types.js';
+
+type ResumedSessionDataWithNotice = ResumedSessionData & {
+ resumeNotice?: string;
+};
describe('useSessionResume', () => {
// Mock dependencies
@@ -75,7 +80,7 @@ describe('useSessionResume', () => {
{ role: 'model' as const, parts: [{ text: 'Hi there!' }] },
];
- const resumedData: ResumedSessionData = {
+ const resumedData: ResumedSessionDataWithNotice = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
@@ -130,7 +135,7 @@ describe('useSessionResume', () => {
const clientHistory = [
{ role: 'user' as const, parts: [{ text: 'Hello' }] },
];
- const resumedData: ResumedSessionData = {
+ const resumedData: ResumedSessionDataWithNotice = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
@@ -157,7 +162,7 @@ describe('useSessionResume', () => {
it('should handle empty history arrays', async () => {
const { result } = renderHook(() => useSessionResume(getDefaultProps()));
- const resumedData: ResumedSessionData = {
+ const resumedData: ResumedSessionDataWithNotice = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
@@ -178,6 +183,35 @@ describe('useSessionResume', () => {
expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith([], resumedData);
});
+ it('should add an info notice to history when resumeNotice is provided', async () => {
+ const { result } = renderHook(() => useSessionResume(getDefaultProps()));
+
+ const resumedData: ResumedSessionDataWithNotice = {
+ conversation: {
+ sessionId: 'test-123',
+ projectHash: 'project-123',
+ startTime: '2025-01-01T00:00:00Z',
+ lastUpdated: '2025-01-01T01:00:00Z',
+ messages: [] as MessageRecord[],
+ },
+ filePath: '/path/to/session.json',
+ resumeNotice: 'Resumed most recent session in this folder: test-session-abcde',
+ };
+
+ await act(async () => {
+ await result.current.loadHistoryForResume([], [], resumedData);
+ });
+
+ expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
+ {
+ type: MessageType.INFO,
+ text: resumedData.resumeNotice,
+ },
+ expect.any(Number),
+ true,
+ );
+ });
+
it('should restore directories from resumed session data', async () => {
const mockAddDirectories = vi
.fn()
diff --git a/packages/cli/src/ui/hooks/useSessionResume.ts b/packages/cli/src/ui/hooks/useSessionResume.ts
index 9889c4bd12..e7f314f741 100644
--- a/packages/cli/src/ui/hooks/useSessionResume.ts
+++ b/packages/cli/src/ui/hooks/useSessionResume.ts
@@ -11,17 +11,22 @@ import {
type ResumedSessionData,
} from '@google/gemini-cli-core';
import type { Part } from '@google/genai';
+import { MessageType } from '../types.js';
import type { HistoryItemWithoutId } from '../types.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import { convertSessionToHistoryFormats } from './useSessionBrowser.js';
+type ResumedSessionDataWithNotice = ResumedSessionData & {
+ resumeNotice?: string;
+};
+
interface UseSessionResumeParams {
config: Config;
historyManager: UseHistoryManagerReturn;
refreshStatic: () => void;
isGeminiClientInitialized: boolean;
setQuittingMessages: (messages: null) => void;
- resumedSessionData?: ResumedSessionData;
+ resumedSessionData?: ResumedSessionDataWithNotice;
isAuthenticating: boolean;
}
@@ -54,7 +59,7 @@ export function useSessionResume({
async (
uiHistory: HistoryItemWithoutId[],
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>,
- resumedData: ResumedSessionData,
+ resumedData: ResumedSessionDataWithNotice,
) => {
// Wait for the client.
if (!isGeminiClientInitialized) {
@@ -84,6 +89,17 @@ export function useSessionResume({
// Give the history to the Gemini client.
await config.getGeminiClient()?.resumeChat(clientHistory, resumedData);
+
+ if (resumedData.resumeNotice) {
+ historyManagerRef.current.addItem(
+ {
+ type: MessageType.INFO,
+ text: resumedData.resumeNotice,
+ },
+ Date.now(),
+ true,
+ );
+ }
} catch (error) {
coreEvents.emitFeedback(
'error',
diff --git a/packages/cli/src/utils/sessionUtils.test.ts b/packages/cli/src/utils/sessionUtils.test.ts
index 01adfd5442..ad5ded169f 100644
--- a/packages/cli/src/utils/sessionUtils.test.ts
+++ b/packages/cli/src/utils/sessionUtils.test.ts
@@ -6,6 +6,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
+ RESUME_LAST_IN_CURRENT_FOLDER,
SessionSelector,
extractFirstUserMessage,
formatRelativeTime,
@@ -49,11 +50,14 @@ describe('SessionSelector', () => {
}
});
- const createChatsDir = async () => {
- const projectDir = path.join(tmpDir, 'project-a');
+ const createChatsDir = async (
+ projectId = 'project-a',
+ root = projectRoot,
+ ) => {
+ const projectDir = path.join(tmpDir, projectId);
const chatsDir = path.join(projectDir, 'chats');
await fs.mkdir(chatsDir, { recursive: true });
- await fs.writeFile(path.join(projectDir, '.project_root'), projectRoot);
+ await fs.writeFile(path.join(projectDir, '.project_root'), root);
return chatsDir;
};
@@ -245,6 +249,146 @@ describe('SessionSelector', () => {
expect(result.sessionData.messages[0].content).toBe('Latest session');
});
+ it('should resolve the latest session from the current folder for bare --resume', async () => {
+ const sessionInCurrentFolderOld = randomUUID();
+ const sessionInCurrentFolderLatest = randomUUID();
+ const sessionInOtherFolderLatest = randomUUID();
+
+ const projectAChatsDir = await createChatsDir('project-a', projectRoot);
+ const projectBChatsDir = await createChatsDir(
+ 'project-b',
+ '/workspace/project-b',
+ );
+
+ await fs.writeFile(
+ path.join(
+ projectAChatsDir,
+ `${SESSION_FILE_PREFIX}2024-01-01T10-00-${sessionInCurrentFolderOld.slice(0, 8)}.json`,
+ ),
+ JSON.stringify(
+ {
+ sessionId: sessionInCurrentFolderOld,
+ projectHash: 'project-a-hash',
+ startTime: '2024-01-01T10:00:00.000Z',
+ lastUpdated: '2024-01-01T10:05:00.000Z',
+ messages: [
+ {
+ type: 'user',
+ content: 'old current-folder session',
+ id: 'msg1',
+ timestamp: '2024-01-01T10:00:00.000Z',
+ },
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+
+ await fs.writeFile(
+ path.join(
+ projectAChatsDir,
+ `${SESSION_FILE_PREFIX}2024-01-01T11-00-${sessionInCurrentFolderLatest.slice(0, 8)}.json`,
+ ),
+ JSON.stringify(
+ {
+ sessionId: sessionInCurrentFolderLatest,
+ projectHash: 'project-a-hash',
+ startTime: '2024-01-01T11:00:00.000Z',
+ lastUpdated: '2024-01-01T11:30:00.000Z',
+ messages: [
+ {
+ type: 'user',
+ content: 'latest current-folder session',
+ id: 'msg1',
+ timestamp: '2024-01-01T11:00:00.000Z',
+ },
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+
+ await fs.writeFile(
+ path.join(
+ projectBChatsDir,
+ `${SESSION_FILE_PREFIX}2024-01-01T12-00-${sessionInOtherFolderLatest.slice(0, 8)}.json`,
+ ),
+ JSON.stringify(
+ {
+ sessionId: sessionInOtherFolderLatest,
+ projectHash: 'project-b-hash',
+ startTime: '2024-01-01T12:00:00.000Z',
+ lastUpdated: '2024-01-01T12:30:00.000Z',
+ messages: [
+ {
+ type: 'user',
+ content: 'latest other-folder session',
+ id: 'msg1',
+ timestamp: '2024-01-01T12:00:00.000Z',
+ },
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+
+ const sessionSelector = new SessionSelector(config);
+ const result = await sessionSelector.resolveSession(
+ RESUME_LAST_IN_CURRENT_FOLDER,
+ );
+
+ expect(result.sessionData.sessionId).toBe(sessionInCurrentFolderLatest);
+ expect(result.resumeNotice).toContain(
+ 'Resumed most recent session in this folder',
+ );
+ });
+
+ it('should fall back to global latest when current folder has no sessions for bare --resume', async () => {
+ const sessionInOtherFolderLatest = randomUUID();
+ const projectBChatsDir = await createChatsDir(
+ 'project-b',
+ '/workspace/project-b',
+ );
+
+ await fs.writeFile(
+ path.join(
+ projectBChatsDir,
+ `${SESSION_FILE_PREFIX}2024-01-01T12-00-${sessionInOtherFolderLatest.slice(0, 8)}.json`,
+ ),
+ JSON.stringify(
+ {
+ sessionId: sessionInOtherFolderLatest,
+ projectHash: 'project-b-hash',
+ startTime: '2024-01-01T12:00:00.000Z',
+ lastUpdated: '2024-01-01T12:30:00.000Z',
+ messages: [
+ {
+ type: 'user',
+ content: 'latest other-folder session',
+ id: 'msg1',
+ timestamp: '2024-01-01T12:00:00.000Z',
+ },
+ ],
+ },
+ null,
+ 2,
+ ),
+ );
+
+ const sessionSelector = new SessionSelector(config);
+ const result = await sessionSelector.resolveSession(
+ RESUME_LAST_IN_CURRENT_FOLDER,
+ );
+
+ expect(result.sessionData.sessionId).toBe(sessionInOtherFolderLatest);
+ expect(result.resumeNotice).toContain(
+ 'No previous sessions found in this folder',
+ );
+ });
+
it('should deduplicate sessions by ID', async () => {
const sessionId = randomUUID();
diff --git a/packages/cli/src/utils/sessionUtils.ts b/packages/cli/src/utils/sessionUtils.ts
index 883026e761..735395bdb3 100644
--- a/packages/cli/src/utils/sessionUtils.ts
+++ b/packages/cli/src/utils/sessionUtils.ts
@@ -31,9 +31,14 @@ import {
/**
* Constant for the resume "latest" identifier.
- * Used when --resume is passed without a value to select the most recent session.
+ * Used when --resume latest is passed to select the most recent session globally.
*/
export const RESUME_LATEST = 'latest';
+/**
+ * Internal identifier used when --resume is passed without a value.
+ * Prefers the most recent session from the current folder, falling back to global latest.
+ */
+export const RESUME_LAST_IN_CURRENT_FOLDER = '__last_in_current_folder__';
/**
* Error codes for session-related errors.
@@ -157,6 +162,7 @@ export interface SessionSelectionResult {
sessionPath: string;
sessionData: ConversationRecord;
displayInfo: string;
+ resumeNotice?: string;
}
export interface RenameSessionResult {
@@ -638,6 +644,35 @@ export async function deleteSessionArtifacts(session: SessionInfo): Promise
+ new Date(a.startTime).getTime() - new Date(b.startTime).getTime(),
+ );
+ return sorted[sorted.length - 1];
+ }
+
+ private getLatestSessionFromCurrentFolder(
+ sessions: SessionInfo[],
+ ): SessionInfo | undefined {
+ const currentProjectRoot = this.config.getProjectRoot();
+ const matchingSessions = sessions.filter(
+ (session) =>
+ session.projectRoot &&
+ path.resolve(session.projectRoot) === path.resolve(currentProjectRoot),
+ );
+
+ if (matchingSessions.length === 0) {
+ return undefined;
+ }
+
+ const sorted = [...matchingSessions].sort(
+ (a, b) =>
+ new Date(a.lastUpdated).getTime() - new Date(b.lastUpdated).getTime(),
+ );
+ return sorted[sorted.length - 1];
+ }
+
/**
* Lists all available sessions globally across projects.
*/
@@ -707,20 +742,31 @@ export class SessionSelector {
async resolveSession(resumeArg: string): Promise {
let selectedSession: SessionInfo;
- if (resumeArg === RESUME_LATEST) {
+ let resumeNotice: string | undefined;
+ if (resumeArg === RESUME_LAST_IN_CURRENT_FOLDER) {
const sessions = await this.listSessions();
if (sessions.length === 0) {
throw new Error('No previous sessions found.');
}
- // Sort by startTime (oldest first, so newest sessions get highest numbers)
- sessions.sort(
- (a, b) =>
- new Date(a.startTime).getTime() - new Date(b.startTime).getTime(),
- );
+ const currentFolderSession =
+ this.getLatestSessionFromCurrentFolder(sessions);
+ if (currentFolderSession) {
+ selectedSession = currentFolderSession;
+ resumeNotice = `Resumed most recent session in this folder: ${selectedSession.sessionName}`;
+ } else {
+ selectedSession = this.getLatestSession(sessions);
+ resumeNotice = `No previous sessions found in this folder. Resumed latest global session: ${selectedSession.sessionName}`;
+ }
+ } else if (resumeArg === RESUME_LATEST) {
+ const sessions = await this.listSessions();
- selectedSession = sessions[sessions.length - 1];
+ if (sessions.length === 0) {
+ throw new Error('No previous sessions found.');
+ }
+
+ selectedSession = this.getLatestSession(sessions);
} else {
try {
selectedSession = await this.findSession(resumeArg);
@@ -736,7 +782,7 @@ export class SessionSelector {
}
}
- return this.selectSession(selectedSession);
+ return this.selectSession(selectedSession, resumeNotice);
}
/**
@@ -744,6 +790,7 @@ export class SessionSelector {
*/
private async selectSession(
sessionInfo: SessionInfo,
+ resumeNotice?: string,
): Promise {
const sessionPath = sessionInfo.sessionPath;
@@ -758,6 +805,7 @@ export class SessionSelector {
sessionPath: sessionInfo.sessionPath,
sessionData,
displayInfo,
+ resumeNotice,
};
} catch (error) {
throw new Error(