diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts index 31ac8bbaa3f89..4129b67b595e8 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts @@ -941,6 +941,18 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes this._stream?.progress(l10n.t('Compacting conversation...')); await this._sdkSession.initializeAndValidateTools(); this._sdkSession.currentMode = 'interactive'; + // Mirror the Copilot CLI SDK's own `messages.length < 2` guard to + // avoid its "Nothing to compact." throw, while distinguishing + // empty sessions from already-compacted sessions in the UI. + const messages = await this._sdkSession.getChatMessages(); + if (messages.length === 0) { + this._stream?.markdown(l10n.t('Nothing to compact.')); + break; + } + if (messages.length < 2) { + this._stream?.markdown(l10n.t('Conversation already compacted.')); + break; + } const result = await this._sdkSession.compactHistory(); if (result.success) { this._stream?.markdown(l10n.t('Compacted conversation.')); diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts index 364be208601d4..36d5d54ce72ce 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts @@ -124,6 +124,9 @@ class MockSdkSession { async compactHistory() { return { success: true }; } + public chatMessages: Awaited> = [{ role: 'user', content: 'hi' }, { role: 'assistant', content: 'hello' }]; + async getChatMessages() { return this.chatMessages; } + async abort() { } isAbortable(): boolean { return true; } @@ -730,6 +733,36 @@ describe('CopilotCLISession', () => { expect(sdkSession.currentMode).toBe('interactive'); expect(stream.output.join('\n')).toContain('Compacted conversation.'); }); + + it('reports already-compacted when no new messages since last compaction (issue #311422)', async () => { + const session = await createSession(); + const stream = new MockChatResponseStream(); + session.attachStream(stream); + // Simulate post-compaction state: only the single summary message remains. + sdkSession.chatMessages = [{ role: 'system', content: 'summary' }]; + let compactCalled = false; + sdkSession.compactHistory = async () => { compactCalled = true; return { success: true }; }; + + await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { command: 'compact', prompt: '' }, [], undefined, authInfo, CancellationToken.None); + + expect(compactCalled).toBe(false); + expect(stream.output.join('\n')).toContain('Conversation already compacted.'); + }); + + it('reports nothing-to-compact on an empty session', async () => { + const session = await createSession(); + const stream = new MockChatResponseStream(); + session.attachStream(stream); + // Simulate a brand-new session with no conversation yet. + sdkSession.chatMessages = []; + let compactCalled = false; + sdkSession.compactHistory = async () => { compactCalled = true; return { success: true }; }; + + await session.handleRequest({ id: '', toolInvocationToken: undefined as never }, { command: 'compact', prompt: '' }, [], undefined, authInfo, CancellationToken.None); + + expect(compactCalled).toBe(false); + expect(stream.output.join('\n')).toContain('Nothing to compact.'); + }); }); describe('steering (sending messages to a busy session)', () => {