summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/commands
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/commands')
-rw-r--r--packages/cli/src/ui/commands/compressCommand.test.ts129
-rw-r--r--packages/cli/src/ui/commands/compressCommand.ts77
-rw-r--r--packages/cli/src/ui/commands/types.ts14
3 files changed, 215 insertions, 5 deletions
diff --git a/packages/cli/src/ui/commands/compressCommand.test.ts b/packages/cli/src/ui/commands/compressCommand.test.ts
new file mode 100644
index 00000000..95a8bda3
--- /dev/null
+++ b/packages/cli/src/ui/commands/compressCommand.test.ts
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { GeminiClient } from '@google/gemini-cli-core';
+import { vi, describe, it, expect, beforeEach } from 'vitest';
+import { compressCommand } from './compressCommand.js';
+import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
+import { MessageType } from '../types.js';
+
+describe('compressCommand', () => {
+ let context: ReturnType<typeof createMockCommandContext>;
+ let mockTryCompressChat: ReturnType<typeof vi.fn>;
+
+ beforeEach(() => {
+ mockTryCompressChat = vi.fn();
+ context = createMockCommandContext({
+ services: {
+ config: {
+ getGeminiClient: () =>
+ ({
+ tryCompressChat: mockTryCompressChat,
+ }) as unknown as GeminiClient,
+ },
+ },
+ });
+ });
+
+ it('should do nothing if a compression is already pending', async () => {
+ context.ui.pendingItem = {
+ type: MessageType.COMPRESSION,
+ compression: {
+ isPending: true,
+ originalTokenCount: null,
+ newTokenCount: null,
+ },
+ };
+ await compressCommand.action!(context, '');
+ expect(context.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: MessageType.ERROR,
+ text: 'Already compressing, wait for previous request to complete',
+ }),
+ expect.any(Number),
+ );
+ expect(context.ui.setPendingItem).not.toHaveBeenCalled();
+ expect(mockTryCompressChat).not.toHaveBeenCalled();
+ });
+
+ it('should set pending item, call tryCompressChat, and add result on success', async () => {
+ const compressedResult = {
+ originalTokenCount: 200,
+ newTokenCount: 100,
+ };
+ mockTryCompressChat.mockResolvedValue(compressedResult);
+
+ await compressCommand.action!(context, '');
+
+ expect(context.ui.setPendingItem).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ type: MessageType.COMPRESSION,
+ compression: {
+ isPending: true,
+ originalTokenCount: null,
+ newTokenCount: null,
+ },
+ }),
+ );
+
+ expect(mockTryCompressChat).toHaveBeenCalledWith(
+ expect.stringMatching(/^compress-\d+$/),
+ true,
+ );
+
+ expect(context.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: MessageType.COMPRESSION,
+ compression: {
+ isPending: false,
+ originalTokenCount: 200,
+ newTokenCount: 100,
+ },
+ }),
+ expect.any(Number),
+ );
+
+ expect(context.ui.setPendingItem).toHaveBeenNthCalledWith(2, null);
+ });
+
+ it('should add an error message if tryCompressChat returns falsy', async () => {
+ mockTryCompressChat.mockResolvedValue(null);
+
+ await compressCommand.action!(context, '');
+
+ expect(context.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: MessageType.ERROR,
+ text: 'Failed to compress chat history.',
+ }),
+ expect.any(Number),
+ );
+ expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
+ });
+
+ it('should add an error message if tryCompressChat throws', async () => {
+ const error = new Error('Compression failed');
+ mockTryCompressChat.mockRejectedValue(error);
+
+ await compressCommand.action!(context, '');
+
+ expect(context.ui.addItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: MessageType.ERROR,
+ text: `Failed to compress chat history: ${error.message}`,
+ }),
+ expect.any(Number),
+ );
+ expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
+ });
+
+ it('should clear the pending item in a finally block', async () => {
+ mockTryCompressChat.mockRejectedValue(new Error('some error'));
+ await compressCommand.action!(context, '');
+ expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
+ });
+});
diff --git a/packages/cli/src/ui/commands/compressCommand.ts b/packages/cli/src/ui/commands/compressCommand.ts
new file mode 100644
index 00000000..c3dfdf37
--- /dev/null
+++ b/packages/cli/src/ui/commands/compressCommand.ts
@@ -0,0 +1,77 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { HistoryItemCompression, MessageType } from '../types.js';
+import { SlashCommand } from './types.js';
+
+export const compressCommand: SlashCommand = {
+ name: 'compress',
+ altName: 'summarize',
+ description: 'Compresses the context by replacing it with a summary.',
+ action: async (context) => {
+ const { ui } = context;
+ if (ui.pendingItem) {
+ ui.addItem(
+ {
+ type: MessageType.ERROR,
+ text: 'Already compressing, wait for previous request to complete',
+ },
+ Date.now(),
+ );
+ return;
+ }
+
+ const pendingMessage: HistoryItemCompression = {
+ type: MessageType.COMPRESSION,
+ compression: {
+ isPending: true,
+ originalTokenCount: null,
+ newTokenCount: null,
+ },
+ };
+
+ try {
+ ui.setPendingItem(pendingMessage);
+ const promptId = `compress-${Date.now()}`;
+ const compressed = await context.services.config
+ ?.getGeminiClient()
+ ?.tryCompressChat(promptId, true);
+ if (compressed) {
+ ui.addItem(
+ {
+ type: MessageType.COMPRESSION,
+ compression: {
+ isPending: false,
+ originalTokenCount: compressed.originalTokenCount,
+ newTokenCount: compressed.newTokenCount,
+ },
+ } as HistoryItemCompression,
+ Date.now(),
+ );
+ } else {
+ ui.addItem(
+ {
+ type: MessageType.ERROR,
+ text: 'Failed to compress chat history.',
+ },
+ Date.now(),
+ );
+ }
+ } catch (e) {
+ ui.addItem(
+ {
+ type: MessageType.ERROR,
+ text: `Failed to compress chat history: ${
+ e instanceof Error ? e.message : String(e)
+ }`,
+ },
+ Date.now(),
+ );
+ } finally {
+ ui.setPendingItem(null);
+ }
+ },
+};
diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts
index 27db2be2..85a85abe 100644
--- a/packages/cli/src/ui/commands/types.ts
+++ b/packages/cli/src/ui/commands/types.ts
@@ -23,11 +23,6 @@ export interface CommandContext {
};
// UI state and history management
ui: {
- // TODO - As more commands are add some additions may be needed or reworked using this new context.
- // Ex.
- // history: HistoryItem[];
- // pendingHistoryItems: HistoryItemWithoutId[];
-
/** Adds a new item to the history display. */
addItem: UseHistoryManagerReturn['addItem'];
/** Clears all history items and the console screen. */
@@ -36,6 +31,15 @@ export interface CommandContext {
* Sets the transient debug message displayed in the application footer in debug mode.
*/
setDebugMessage: (message: string) => void;
+ /** The currently pending history item, if any. */
+ pendingItem: HistoryItemWithoutId | null;
+ /**
+ * Sets a pending item in the history, which is useful for indicating
+ * that a long-running operation is in progress.
+ *
+ * @param item The history item to display as pending, or `null` to clear.
+ */
+ setPendingItem: (item: HistoryItemWithoutId | null) => void;
};
// Session-specific data
session: {