fix(cli): reset plan session state on /clear (#25515)

This commit is contained in:
Jason Matthew Suhari
2026-04-17 03:20:36 +08:00
committed by GitHub
parent 2b6dab6136
commit 9600da2c8f
7 changed files with 171 additions and 3 deletions
+89
View File
@@ -1774,6 +1774,95 @@ describe('Server Config (config.ts)', () => {
expect(config1.topicState.getTopic()).toBe('Topic 1');
expect(config2.topicState.getTopic()).toBe('Topic 2');
});
it('updates storage session-scoped directories when the sessionId changes', async () => {
const config = new Config({
...baseParams,
sessionId: 'session-one',
plan: true,
});
await config.initialize();
const tempDir = config.storage.getProjectTempDir();
const oldPlansDir = path.join(tempDir, 'session-one', 'plans');
const oldTrackerService = config.getTrackerService();
config.setSessionId('session-two');
expect(config.getSessionId()).toBe('session-two');
expect(config.storage.getProjectTempPlansDir()).toBe(
path.join(tempDir, 'session-two', 'plans'),
);
expect(config.storage.getProjectTempTrackerDir()).toBe(
path.join(tempDir, 'session-two', 'tracker'),
);
expect(config.getTrackerService()).not.toBe(oldTrackerService);
expect(config.getTrackerService().trackerDir).toBe(
path.join(tempDir, 'session-two', 'tracker'),
);
expect(config.getWorkspaceContext().getDirectories()).not.toContain(
oldPlansDir,
);
});
it('does not throw when changing sessions before the previous plans dir exists', async () => {
const config = new Config({
...baseParams,
sessionId: 'session-one',
plan: true,
});
await config.initialize();
const missingPlansDir = config.storage.getProjectTempPlansDir();
const realpathMock = vi.mocked(fs.realpathSync);
const originalImplementation = realpathMock.getMockImplementation();
try {
realpathMock.mockImplementation((input) => {
const normalizedInput =
typeof input === 'string' || Buffer.isBuffer(input)
? input
: input.toString();
if (normalizedInput === missingPlansDir) {
const error = new Error(
`ENOENT: no such file or directory, ${normalizedInput}`,
);
Object.assign(error, { code: 'ENOENT' });
throw error;
}
if (originalImplementation) {
return originalImplementation(input);
}
return normalizedInput;
});
expect(() => config.setSessionId('session-two')).not.toThrow();
} finally {
realpathMock.mockImplementation((input) => {
if (originalImplementation) {
return originalImplementation(input);
}
return typeof input === 'string' || Buffer.isBuffer(input)
? input
: input.toString();
});
}
});
it('clears the approved plan when starting a new session', () => {
const config = new Config({
...baseParams,
sessionId: 'session-one',
});
config.setApprovedPlanPath('/tmp/session-one/plans/approved.md');
expect(() => config.resetNewSessionState('session-two')).not.toThrow();
expect(config.getSessionId()).toBe('session-two');
expect(config.getApprovedPlanPath()).toBeUndefined();
});
});
describe('GemmaModelRouterSettings', () => {
+46
View File
@@ -1762,7 +1762,22 @@ export class Config implements McpContext, AgentLoopContext {
}
setSessionId(sessionId: string): void {
const previousPlansDir = this.storage.isInitialized()
? this.storage.getPlansDir()
: undefined;
this._sessionId = sessionId;
this.storage.setSessionId(sessionId);
this.trackerService = undefined;
if (previousPlansDir) {
this.refreshSessionScopedPlansDirectory(previousPlansDir);
}
}
resetNewSessionState(sessionId: string): void {
this.setSessionId(sessionId);
this.approvedPlanPath = undefined;
}
setTerminalBackground(terminalBackground: string | undefined): void {
@@ -2051,6 +2066,37 @@ export class Config implements McpContext, AgentLoopContext {
return getWorkspaceContextOverride() ?? this.workspaceContext;
}
private refreshSessionScopedPlansDirectory(previousPlansDir: string): void {
const nextPlansDir = this.storage.getPlansDir();
if (previousPlansDir === nextPlansDir) {
return;
}
const pathsToRemove = new Set([previousPlansDir]);
try {
pathsToRemove.add(resolveToRealPath(previousPlansDir));
} catch {
// The previous session's plans directory may never have been created.
// In that case there is nothing to resolve or remove beyond the raw path.
}
const currentDirectories = this.workspaceContext
.getDirectories()
.filter((dir) => !pathsToRemove.has(dir));
this.workspaceContext.setDirectories(currentDirectories);
try {
if (fs.existsSync(nextPlansDir)) {
this.workspaceContext.addDirectory(nextPlansDir);
}
} catch {
// Ignore invalid or unreadable plans directories here. This mirrors
// initialization behavior, which only adds the plans directory when it
// already exists and is readable.
}
}
getAgentRegistry(): AgentRegistry {
return this.agentRegistry;
}
+21
View File
@@ -211,6 +211,27 @@ describe('Storage additional helpers', () => {
expect(storageWithSession.getProjectTempTrackerDir()).toBe(expected);
});
it('updates session-scoped directories when the sessionId changes', async () => {
const storageWithSession = new Storage(projectRoot, 'session-one');
ProjectRegistry.prototype.getShortId = vi
.fn()
.mockReturnValue(PROJECT_SLUG);
await storageWithSession.initialize();
const tempDir = storageWithSession.getProjectTempDir();
storageWithSession.setSessionId('session-two');
expect(storageWithSession.getProjectTempPlansDir()).toBe(
path.join(tempDir, 'session-two', 'plans'),
);
expect(storageWithSession.getProjectTempTrackerDir()).toBe(
path.join(tempDir, 'session-two', 'tracker'),
);
expect(storageWithSession.getProjectTempTasksDir()).toBe(
path.join(tempDir, 'session-two', 'tasks'),
);
});
describe('Session and JSON Loading', () => {
beforeEach(async () => {
await storage.initialize();
+9 -1
View File
@@ -28,7 +28,7 @@ export const AUTO_SAVED_POLICY_FILENAME = 'auto-saved.toml';
export class Storage {
private readonly targetDir: string;
private readonly sessionId: string | undefined;
private sessionId: string | undefined;
private projectIdentifier: string | undefined;
private initPromise: Promise<void> | undefined;
private customPlansDir: string | undefined;
@@ -42,6 +42,14 @@ export class Storage {
this.customPlansDir = dir;
}
setSessionId(sessionId: string | undefined): void {
this.sessionId = sessionId;
}
isInitialized(): boolean {
return !!this.projectIdentifier;
}
static getGlobalGeminiDir(): string {
const homeDir = homedir();
if (!homeDir) {