Skip to content

Commit 87d252b

Browse files
committed
feat: allow queuing messages during compression (#24071)
1 parent 1d72a12 commit 87d252b

4 files changed

Lines changed: 109 additions & 39 deletions

File tree

packages/cli/src/ui/AppContainer.test.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ import { type LoadedSettings } from '../config/settings.js';
100100
import { createMockSettings } from '../test-utils/settings.js';
101101
import type { InitializationResult } from '../core/initializer.js';
102102
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
103-
import { StreamingState } from './types.js';
103+
import { StreamingState, MessageType } from './types.js';
104104
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
105105
import {
106106
UIActionsContext,
@@ -3576,4 +3576,48 @@ describe('AppContainer State Management', () => {
35763576
unmount();
35773577
});
35783578
});
3579+
3580+
describe('Compression Queuing', () => {
3581+
it('queues messages during compression instead of handling as steering hints', async () => {
3582+
const { checkPermissions } = await import(
3583+
'./hooks/atCommandProcessor.js'
3584+
);
3585+
vi.mocked(checkPermissions).mockResolvedValue([]);
3586+
3587+
vi.spyOn(mockConfig, 'isModelSteeringEnabled').mockReturnValue(true);
3588+
3589+
const actual = await vi.importActual('./hooks/useMessageQueue.js');
3590+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3591+
const { useMessageQueue: realUseMessageQueue } = actual as any;
3592+
mockedUseMessageQueue.mockImplementation(realUseMessageQueue);
3593+
3594+
// Start compression by mocking pendingHistoryItems to include a pending compression
3595+
mockedUseGeminiStream.mockImplementation(() => ({
3596+
...DEFAULT_GEMINI_STREAM_MOCK,
3597+
pendingHistoryItems: [
3598+
{
3599+
type: MessageType.COMPRESSION,
3600+
compression: {
3601+
isPending: true,
3602+
originalTokenCount: null,
3603+
newTokenCount: null,
3604+
compressionStatus: null,
3605+
},
3606+
},
3607+
],
3608+
}));
3609+
3610+
const { unmount } = await act(async () => renderAppContainer());
3611+
3612+
// Submit a message
3613+
await act(async () =>
3614+
capturedUIActions.handleFinalSubmit('follow up message'),
3615+
);
3616+
3617+
// Verify it was queued, not submitted as steering hint
3618+
expect(capturedUIState.messageQueue).toContain('follow up message');
3619+
3620+
unmount();
3621+
});
3622+
});
35793623
});

packages/cli/src/ui/AppContainer.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
13101310

13111311
const { isMcpReady } = useMcpStatus(config);
13121312

1313+
const isCompressing = useMemo(
1314+
() =>
1315+
pendingHistoryItems.some(
1316+
(item) =>
1317+
item.type === MessageType.COMPRESSION && item.compression.isPending,
1318+
),
1319+
[pendingHistoryItems],
1320+
);
1321+
13131322
const {
13141323
messageQueue,
13151324
addMessage,
@@ -1321,6 +1330,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
13211330
streamingState,
13221331
submitQuery,
13231332
isMcpReady,
1333+
isCompressing,
13241334
});
13251335

13261336
cancelHandlerRef.current = useCallback(
@@ -1415,7 +1425,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
14151425
}
14161426

14171427
const isMcpOrConfigReady = isConfigInitialized && isMcpReady;
1418-
if ((isSlash && isConfigInitialized) || (isIdle && isMcpOrConfigReady)) {
1428+
if (
1429+
(isSlash && isConfigInitialized) ||
1430+
(isIdle && !isCompressing && isMcpOrConfigReady)
1431+
) {
14191432
if (!isSlash) {
14201433
const permissions = await checkPermissions(submittedValue, config);
14211434
if (permissions.length > 0) {
@@ -1438,7 +1451,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
14381451
void submitQuery(submittedValue);
14391452
} else {
14401453
// Check messageQueue.length === 0 to only notify on the first queued item
1441-
if (isIdle && !isMcpOrConfigReady && messageQueue.length === 0) {
1454+
if (
1455+
isIdle &&
1456+
!isCompressing &&
1457+
!isMcpOrConfigReady &&
1458+
messageQueue.length === 0
1459+
) {
14421460
coreEvents.emitFeedback(
14431461
'info',
14441462
!isConfigInitialized
@@ -1458,6 +1476,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
14581476
slashCommands,
14591477
isMcpReady,
14601478
streamingState,
1479+
isCompressing,
14611480
messageQueue.length,
14621481
pendingHistoryItems,
14631482
config,

packages/cli/src/ui/commands/compressCommand.ts

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const compressCommand: SlashCommand = {
1313
description: 'Compresses the context by replacing it with a summary',
1414
kind: CommandKind.BUILT_IN,
1515
autoExecute: true,
16-
action: async (context) => {
16+
action: (context) => {
1717
const { ui } = context;
1818
if (ui.pendingItem) {
1919
ui.addItem(
@@ -36,48 +36,51 @@ export const compressCommand: SlashCommand = {
3636
},
3737
};
3838

39-
try {
40-
ui.setPendingItem(pendingMessage);
41-
const promptId = `compress-${Date.now()}`;
42-
const compressed =
43-
await context.services.agentContext?.geminiClient?.tryCompressChat(
44-
promptId,
45-
true,
46-
);
47-
if (compressed) {
48-
ui.addItem(
49-
{
50-
type: MessageType.COMPRESSION,
51-
compression: {
52-
isPending: false,
53-
originalTokenCount: compressed.originalTokenCount,
54-
newTokenCount: compressed.newTokenCount,
55-
compressionStatus: compressed.compressionStatus,
39+
ui.setPendingItem(pendingMessage);
40+
41+
void (async () => {
42+
try {
43+
const promptId = `compress-${Date.now()}`;
44+
const compressed =
45+
await context.services.agentContext?.geminiClient?.tryCompressChat(
46+
promptId,
47+
true,
48+
);
49+
if (compressed) {
50+
ui.addItem(
51+
{
52+
type: MessageType.COMPRESSION,
53+
compression: {
54+
isPending: false,
55+
originalTokenCount: compressed.originalTokenCount,
56+
newTokenCount: compressed.newTokenCount,
57+
compressionStatus: compressed.compressionStatus,
58+
},
59+
} as HistoryItemCompression,
60+
Date.now(),
61+
);
62+
} else {
63+
ui.addItem(
64+
{
65+
type: MessageType.ERROR,
66+
text: 'Failed to compress chat history.',
5667
},
57-
} as HistoryItemCompression,
58-
Date.now(),
59-
);
60-
} else {
68+
Date.now(),
69+
);
70+
}
71+
} catch (e) {
6172
ui.addItem(
6273
{
6374
type: MessageType.ERROR,
64-
text: 'Failed to compress chat history.',
75+
text: `Failed to compress chat history: ${
76+
e instanceof Error ? e.message : String(e)
77+
}`,
6578
},
6679
Date.now(),
6780
);
81+
} finally {
82+
ui.setPendingItem(null);
6883
}
69-
} catch (e) {
70-
ui.addItem(
71-
{
72-
type: MessageType.ERROR,
73-
text: `Failed to compress chat history: ${
74-
e instanceof Error ? e.message : String(e)
75-
}`,
76-
},
77-
Date.now(),
78-
);
79-
} finally {
80-
ui.setPendingItem(null);
81-
}
84+
})();
8285
},
8386
};

packages/cli/src/ui/hooks/useMessageQueue.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface UseMessageQueueOptions {
1212
streamingState: StreamingState;
1313
submitQuery: (query: string) => void;
1414
isMcpReady: boolean;
15+
isCompressing?: boolean;
1516
}
1617

1718
export interface UseMessageQueueReturn {
@@ -32,6 +33,7 @@ export function useMessageQueue({
3233
streamingState,
3334
submitQuery,
3435
isMcpReady,
36+
isCompressing = false,
3537
}: UseMessageQueueOptions): UseMessageQueueReturn {
3638
const [messageQueue, setMessageQueue] = useState<string[]>([]);
3739

@@ -69,6 +71,7 @@ export function useMessageQueue({
6971
if (
7072
isConfigInitialized &&
7173
streamingState === StreamingState.Idle &&
74+
!isCompressing &&
7275
isMcpReady &&
7376
messageQueue.length > 0
7477
) {
@@ -84,6 +87,7 @@ export function useMessageQueue({
8487
isMcpReady,
8588
messageQueue,
8689
submitQuery,
90+
isCompressing,
8791
]);
8892

8993
return {

0 commit comments

Comments
 (0)