mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-05 07:37:50 -07:00
Merge branch 'main' into worktree-con-plan-bug
Resolves conflict in packages/core/src/tools/enter-plan-mode.test.ts by removing an assertion for directory creation, which has been centralized in config.ts in this branch.
This commit is contained in:
@@ -24,10 +24,24 @@ export function registerCleanup(fn: (() => void) | (() => Promise<void>)) {
|
||||
cleanupFunctions.push(fn);
|
||||
}
|
||||
|
||||
export function removeCleanup(fn: (() => void) | (() => Promise<void>)) {
|
||||
const index = cleanupFunctions.indexOf(fn);
|
||||
if (index !== -1) {
|
||||
cleanupFunctions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSyncCleanup(fn: () => void) {
|
||||
syncCleanupFunctions.push(fn);
|
||||
}
|
||||
|
||||
export function removeSyncCleanup(fn: () => void) {
|
||||
const index = syncCleanupFunctions.indexOf(fn);
|
||||
if (index !== -1) {
|
||||
syncCleanupFunctions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the internal cleanup state for testing purposes.
|
||||
* This allows tests to run in isolation without vi.resetModules().
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from './sessionUtils.js';
|
||||
import {
|
||||
SESSION_FILE_PREFIX,
|
||||
type Config,
|
||||
type Storage,
|
||||
type MessageRecord,
|
||||
CoreToolCallStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -25,20 +25,17 @@ import { randomUUID } from 'node:crypto';
|
||||
|
||||
describe('SessionSelector', () => {
|
||||
let tmpDir: string;
|
||||
let config: Config;
|
||||
let storage: Storage;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a temporary directory for testing
|
||||
tmpDir = path.join(process.cwd(), '.tmp-test-sessions');
|
||||
await fs.mkdir(tmpDir, { recursive: true });
|
||||
|
||||
// Mock config
|
||||
config = {
|
||||
storage: {
|
||||
getProjectTempDir: () => tmpDir,
|
||||
},
|
||||
getSessionId: () => 'current-session-id',
|
||||
} as Partial<Config> as Config;
|
||||
// Mock storage
|
||||
storage = {
|
||||
getProjectTempDir: () => tmpDir,
|
||||
} as Partial<Storage> as Storage;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -104,7 +101,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(session2, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
|
||||
// Test resolving by UUID
|
||||
const result1 = await sessionSelector.resolveSession(sessionId1);
|
||||
@@ -170,7 +167,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(session2, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
|
||||
// Test resolving by index (1-based)
|
||||
const result1 = await sessionSelector.resolveSession('1');
|
||||
@@ -234,7 +231,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(session2, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
|
||||
// Test resolving latest
|
||||
const result = await sessionSelector.resolveSession('latest');
|
||||
@@ -271,7 +268,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(session, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
|
||||
// Test resolving by UUID with leading/trailing spaces
|
||||
const result = await sessionSelector.resolveSession(` ${sessionId} `);
|
||||
@@ -334,7 +331,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(sessionDuplicate, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
expect(sessions.length).toBe(1);
|
||||
@@ -373,7 +370,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(session1, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
|
||||
await expect(
|
||||
sessionSelector.resolveSession('invalid-uuid'),
|
||||
@@ -389,14 +386,11 @@ describe('SessionSelector', () => {
|
||||
const chatsDir = path.join(tmpDir, 'chats');
|
||||
await fs.mkdir(chatsDir, { recursive: true });
|
||||
|
||||
const emptyConfig = {
|
||||
storage: {
|
||||
getProjectTempDir: () => tmpDir,
|
||||
},
|
||||
getSessionId: () => 'current-session-id',
|
||||
} as Partial<Config> as Config;
|
||||
const emptyStorage = {
|
||||
getProjectTempDir: () => tmpDir,
|
||||
} as Partial<Storage> as Storage;
|
||||
|
||||
const sessionSelector = new SessionSelector(emptyConfig);
|
||||
const sessionSelector = new SessionSelector(emptyStorage);
|
||||
|
||||
await expect(sessionSelector.resolveSession('latest')).rejects.toSatisfy(
|
||||
(error) => {
|
||||
@@ -469,7 +463,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(sessionSystemOnly, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
// Should only list the session with user message
|
||||
@@ -508,7 +502,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(sessionGeminiOnly, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
// Should list the session with gemini message
|
||||
@@ -574,7 +568,7 @@ describe('SessionSelector', () => {
|
||||
JSON.stringify(subagentSession, null, 2),
|
||||
);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
// Should only list the main session
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
partListUnionToString,
|
||||
SESSION_FILE_PREFIX,
|
||||
CoreToolCallStatus,
|
||||
type Config,
|
||||
type Storage,
|
||||
type ConversationRecord,
|
||||
type MessageRecord,
|
||||
loadConversationRecord,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
@@ -250,23 +251,27 @@ export const getAllSessionFiles = async (
|
||||
try {
|
||||
const files = await fs.readdir(chatsDir);
|
||||
const sessionFiles = files
|
||||
.filter((f) => f.startsWith(SESSION_FILE_PREFIX) && f.endsWith('.json'))
|
||||
.filter(
|
||||
(f) =>
|
||||
f.startsWith(SESSION_FILE_PREFIX) &&
|
||||
(f.endsWith('.json') || f.endsWith('.jsonl')),
|
||||
)
|
||||
.sort(); // Sort by filename, which includes timestamp
|
||||
|
||||
const sessionPromises = sessionFiles.map(
|
||||
async (file): Promise<SessionFileEntry> => {
|
||||
const filePath = path.join(chatsDir, file);
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const content: ConversationRecord = JSON.parse(
|
||||
await fs.readFile(filePath, 'utf8'),
|
||||
);
|
||||
const content = await loadConversationRecord(filePath, {
|
||||
metadataOnly: !options.includeFullContent,
|
||||
});
|
||||
if (!content) {
|
||||
return { fileName: file, sessionInfo: null };
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!content.sessionId ||
|
||||
!content.messages ||
|
||||
!Array.isArray(content.messages) ||
|
||||
!content.startTime ||
|
||||
!content.lastUpdated
|
||||
) {
|
||||
@@ -275,7 +280,7 @@ export const getAllSessionFiles = async (
|
||||
}
|
||||
|
||||
// Skip sessions that only contain system messages (info, error, warning)
|
||||
if (!hasUserOrAssistantMessage(content.messages)) {
|
||||
if (!content.hasUserOrAssistantMessage) {
|
||||
return { fileName: file, sessionInfo: null };
|
||||
}
|
||||
|
||||
@@ -285,7 +290,9 @@ export const getAllSessionFiles = async (
|
||||
return { fileName: file, sessionInfo: null };
|
||||
}
|
||||
|
||||
const firstUserMessage = extractFirstUserMessage(content.messages);
|
||||
const firstUserMessage = content.firstUserMessage
|
||||
? cleanMessage(content.firstUserMessage)
|
||||
: extractFirstUserMessage(content.messages);
|
||||
const isCurrentSession = currentSessionId
|
||||
? file.includes(currentSessionId.slice(0, 8))
|
||||
: false;
|
||||
@@ -310,11 +317,11 @@ export const getAllSessionFiles = async (
|
||||
|
||||
const sessionInfo: SessionInfo = {
|
||||
id: content.sessionId,
|
||||
file: file.replace('.json', ''),
|
||||
file: file.replace(/\.jsonl?$/, ''),
|
||||
fileName: file,
|
||||
startTime: content.startTime,
|
||||
lastUpdated: content.lastUpdated,
|
||||
messageCount: content.messages.length,
|
||||
messageCount: content.messageCount ?? content.messages.length,
|
||||
displayName: content.summary
|
||||
? stripUnsafeCharacters(content.summary)
|
||||
: firstUserMessage,
|
||||
@@ -399,17 +406,14 @@ export const getSessionFiles = async (
|
||||
* Utility class for session discovery and selection.
|
||||
*/
|
||||
export class SessionSelector {
|
||||
constructor(private config: Config) {}
|
||||
constructor(private storage: Storage) {}
|
||||
|
||||
/**
|
||||
* Lists all available sessions for the current project.
|
||||
*/
|
||||
async listSessions(): Promise<SessionInfo[]> {
|
||||
const chatsDir = path.join(
|
||||
this.config.storage.getProjectTempDir(),
|
||||
'chats',
|
||||
);
|
||||
return getSessionFiles(chatsDir, this.config.getSessionId());
|
||||
const chatsDir = path.join(this.storage.getProjectTempDir(), 'chats');
|
||||
return getSessionFiles(chatsDir);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -452,10 +456,7 @@ export class SessionSelector {
|
||||
return sortedSessions[index - 1];
|
||||
}
|
||||
|
||||
const chatsDir = path.join(
|
||||
this.config.storage.getProjectTempDir(),
|
||||
'chats',
|
||||
);
|
||||
const chatsDir = path.join(this.storage.getProjectTempDir(), 'chats');
|
||||
throw SessionError.invalidSessionIdentifier(trimmedIdentifier, chatsDir);
|
||||
}
|
||||
|
||||
@@ -507,17 +508,14 @@ export class SessionSelector {
|
||||
private async selectSession(
|
||||
sessionInfo: SessionInfo,
|
||||
): Promise<SessionSelectionResult> {
|
||||
const chatsDir = path.join(
|
||||
this.config.storage.getProjectTempDir(),
|
||||
'chats',
|
||||
);
|
||||
const chatsDir = path.join(this.storage.getProjectTempDir(), 'chats');
|
||||
const sessionPath = path.join(chatsDir, sessionInfo.fileName);
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const sessionData: ConversationRecord = JSON.parse(
|
||||
await fs.readFile(sessionPath, 'utf8'),
|
||||
);
|
||||
const sessionData = await loadConversationRecord(sessionPath);
|
||||
if (!sessionData) {
|
||||
throw new Error('Failed to load session data');
|
||||
}
|
||||
|
||||
const displayInfo = `Session ${sessionInfo.index}: ${sessionInfo.firstUserMessage} (${sessionInfo.messageCount} messages, ${formatRelativeTime(sessionInfo.lastUpdated)})`;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function listSessions(config: Config): Promise<void> {
|
||||
// Generate summary for most recent session if needed
|
||||
await generateSummary(config);
|
||||
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(config.storage);
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
@@ -55,7 +55,7 @@ export async function deleteSession(
|
||||
config: Config,
|
||||
sessionIndex: string,
|
||||
): Promise<void> {
|
||||
const sessionSelector = new SessionSelector(config);
|
||||
const sessionSelector = new SessionSelector(config.storage);
|
||||
const sessions = await sessionSelector.listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user