refactor: make baseTimestamp optional in addItem and remove redundant calls (#16471)

This commit is contained in:
Sehoon Shon
2026-01-13 14:15:04 -05:00
committed by GitHub
parent aa52462550
commit 91fcca3b1c
30 changed files with 528 additions and 888 deletions
@@ -87,20 +87,17 @@ describe('aboutCommand', () => {
await aboutCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ABOUT,
cliVersion: 'test-version',
osVersion: 'test-os',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
gcpProject: 'test-gcp-project',
ideClient: 'test-ide',
userEmail: 'test-email@example.com',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ABOUT,
cliVersion: 'test-version',
osVersion: 'test-os',
sandboxEnv: 'no sandbox',
modelVersion: 'test-model',
selectedAuthType: 'test-auth',
gcpProject: 'test-gcp-project',
ideClient: 'test-ide',
userEmail: 'test-email@example.com',
});
});
it('should show the correct sandbox environment variable', async () => {
@@ -115,7 +112,6 @@ describe('aboutCommand', () => {
expect.objectContaining({
sandboxEnv: 'gemini-sandbox',
}),
expect.any(Number),
);
});
@@ -132,7 +128,6 @@ describe('aboutCommand', () => {
expect.objectContaining({
sandboxEnv: 'sandbox-exec (test-profile)',
}),
expect.any(Number),
);
});
@@ -159,7 +154,6 @@ describe('aboutCommand', () => {
gcpProject: 'test-gcp-project',
ideClient: '',
}),
expect.any(Number),
);
});
});
+1 -1
View File
@@ -56,7 +56,7 @@ export const aboutCommand: SlashCommand = {
userEmail,
};
context.ui.addItem(aboutItem, Date.now());
context.ui.addItem(aboutItem);
},
};
@@ -79,7 +79,6 @@ describe('agentsCommand', () => {
type: MessageType.AGENTS_LIST,
agents: mockAgents,
}),
expect.any(Number),
);
});
@@ -44,7 +44,7 @@ const agentsListCommand: SlashCommand = {
agents,
};
context.ui.addItem(agentsListItem, Date.now());
context.ui.addItem(agentsListItem);
return;
},
@@ -65,13 +65,10 @@ const agentsRefreshCommand: SlashCommand = {
};
}
context.ui.addItem(
{
type: MessageType.INFO,
text: 'Refreshing agent registry...',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: 'Refreshing agent registry...',
});
await agentRegistry.reload();
@@ -127,22 +127,19 @@ describe('chatCommand', () => {
await listCommand?.action?.(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: 'chat_list',
chats: [
{
name: 'test1',
mtime: date1.toISOString(),
},
{
name: 'test2',
mtime: date2.toISOString(),
},
],
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: 'chat_list',
chats: [
{
name: 'test1',
mtime: date1.toISOString(),
},
{
name: 'test2',
mtime: date2.toISOString(),
},
],
});
});
});
describe('save subcommand', () => {
+1 -1
View File
@@ -81,7 +81,7 @@ const listCommand: SlashCommand = {
chats: chatDetails,
};
context.ui.addItem(item, Date.now());
context.ui.addItem(item);
},
};
@@ -94,7 +94,6 @@ describe('directoryCommand', () => {
'/home/user/project1',
)}\n- ${path.normalize('/home/user/project2')}`,
}),
expect.any(Number),
);
});
});
@@ -121,7 +120,6 @@ describe('directoryCommand', () => {
type: MessageType.ERROR,
text: 'Please provide at least one path to add.',
}),
expect.any(Number),
);
});
@@ -135,7 +133,6 @@ describe('directoryCommand', () => {
type: MessageType.INFO,
text: `Successfully added directories:\n- ${newPath}`,
}),
expect.any(Number),
);
});
@@ -151,7 +148,6 @@ describe('directoryCommand', () => {
type: MessageType.INFO,
text: `Successfully added directories:\n- ${newPath1}\n- ${newPath2}`,
}),
expect.any(Number),
);
});
@@ -168,7 +164,6 @@ describe('directoryCommand', () => {
type: MessageType.ERROR,
text: `Error adding '${newPath}': ${error.message}`,
}),
expect.any(Number),
);
});
@@ -191,7 +186,6 @@ describe('directoryCommand', () => {
type: MessageType.INFO,
text: `The following directories are already in the workspace:\n- ${existingPath}`,
}),
expect.any(Number),
);
expect(mockWorkspaceContext.addDirectory).not.toHaveBeenCalledWith(
existingPath,
@@ -218,7 +212,6 @@ describe('directoryCommand', () => {
type: MessageType.INFO,
text: `Successfully added directories:\n- ${validPath}`,
}),
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
@@ -226,7 +219,6 @@ describe('directoryCommand', () => {
type: MessageType.ERROR,
text: `Error adding '${invalidPath}': ${error.message}`,
}),
expect.any(Number),
);
});
@@ -317,7 +309,6 @@ describe('directoryCommand', () => {
type: MessageType.ERROR,
text: expect.stringContaining('explicitly untrusted'),
}),
expect.any(Number),
);
});
@@ -22,18 +22,18 @@ import type { Config } from '@google/gemini-cli-core';
async function finishAddingDirectories(
config: Config,
addItem: (itemData: Omit<HistoryItem, 'id'>, baseTimestamp: number) => number,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
) => number,
added: string[],
errors: string[],
) {
if (!config) {
addItem(
{
type: MessageType.ERROR,
text: 'Configuration is not available.',
},
Date.now(),
);
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
return;
}
@@ -41,13 +41,10 @@ async function finishAddingDirectories(
if (config.shouldLoadMemoryFromIncludeDirectories()) {
await refreshServerHierarchicalMemory(config);
}
addItem(
{
type: MessageType.INFO,
text: `Successfully added GEMINI.md files from the following directories if there are:\n- ${added.join('\n- ')}`,
},
Date.now(),
);
addItem({
type: MessageType.INFO,
text: `Successfully added GEMINI.md files from the following directories if there are:\n- ${added.join('\n- ')}`,
});
} catch (error) {
errors.push(`Error refreshing memory: ${(error as Error).message}`);
}
@@ -57,17 +54,14 @@ async function finishAddingDirectories(
if (gemini) {
await gemini.addDirectoryContext();
}
addItem(
{
type: MessageType.INFO,
text: `Successfully added directories:\n- ${added.join('\n- ')}`,
},
Date.now(),
);
addItem({
type: MessageType.INFO,
text: `Successfully added directories:\n- ${added.join('\n- ')}`,
});
}
if (errors.length > 0) {
addItem({ type: MessageType.ERROR, text: errors.join('\n') }, Date.now());
addItem({ type: MessageType.ERROR, text: errors.join('\n') });
}
}
@@ -112,13 +106,10 @@ export const directoryCommand: SlashCommand = {
const [...rest] = args.split(' ');
if (!config) {
addItem(
{
type: MessageType.ERROR,
text: 'Configuration is not available.',
},
Date.now(),
);
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
return;
}
@@ -136,13 +127,10 @@ export const directoryCommand: SlashCommand = {
.split(',')
.filter((p) => p);
if (pathsToAdd.length === 0) {
addItem(
{
type: MessageType.ERROR,
text: 'Please provide at least one path to add.',
},
Date.now(),
);
addItem({
type: MessageType.ERROR,
text: 'Please provide at least one path to add.',
});
return;
}
@@ -164,15 +152,12 @@ export const directoryCommand: SlashCommand = {
}
if (alreadyAdded.length > 0) {
addItem(
{
type: MessageType.INFO,
text: `The following directories are already in the workspace:\n- ${alreadyAdded.join(
'\n- ',
)}`,
},
Date.now(),
);
addItem({
type: MessageType.INFO,
text: `The following directories are already in the workspace:\n- ${alreadyAdded.join(
'\n- ',
)}`,
});
}
if (pathsToProcess.length === 0) {
@@ -262,25 +247,19 @@ export const directoryCommand: SlashCommand = {
services: { config },
} = context;
if (!config) {
addItem(
{
type: MessageType.ERROR,
text: 'Configuration is not available.',
},
Date.now(),
);
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
return;
}
const workspaceContext = config.getWorkspaceContext();
const directories = workspaceContext.getDirectories();
const directoryList = directories.map((dir) => `- ${dir}`).join('\n');
addItem(
{
type: MessageType.INFO,
text: `Current workspace directories:\n${directoryList}`,
},
Date.now(),
);
addItem({
type: MessageType.INFO,
text: `Current workspace directories:\n${directoryList}`,
});
},
},
],
@@ -148,13 +148,10 @@ describe('extensionsCommand', () => {
if (!command.action) throw new Error('Action not defined');
await command.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
});
});
it('should show a message if no extensions are installed', async () => {
@@ -163,13 +160,10 @@ describe('extensionsCommand', () => {
if (!command.action) throw new Error('Action not defined');
await command.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
});
});
});
@@ -244,26 +238,20 @@ describe('extensionsCommand', () => {
it('should show usage if no args are provided', async () => {
await updateAction(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Usage: /extensions update <extension-names>|--all',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Usage: /extensions update <extension-names>|--all',
});
});
it('should show a message if no extensions are installed', async () => {
mockGetExtensions.mockReturnValue([]);
await updateAction(mockContext, 'ext-one');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
});
});
it('should inform user if there are no extensions to update with --all', async () => {
@@ -276,13 +264,10 @@ describe('extensionsCommand', () => {
);
await updateAction(mockContext, '--all');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: 'No extensions to update.',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: 'No extensions to update.',
});
});
it('should call setPendingItem and addItem in a finally block on success', async () => {
@@ -310,13 +295,10 @@ describe('extensionsCommand', () => {
extensions: expect.any(Array),
});
expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
});
});
it('should call setPendingItem and addItem in a finally block on failure', async () => {
@@ -329,20 +311,14 @@ describe('extensionsCommand', () => {
extensions: expect.any(Array),
});
expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Something went wrong',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Something went wrong',
});
});
it('should update a single extension by name', async () => {
@@ -403,13 +379,10 @@ describe('extensionsCommand', () => {
extensions: expect.any(Array),
});
expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.EXTENSIONS_LIST,
extensions: expect.any(Array),
});
});
});
@@ -430,13 +403,10 @@ describe('extensionsCommand', () => {
await exploreAction(mockContext, '');
const extensionsUrl = 'https://geminicli.com/extensions/';
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Opening extensions page in your browser: ${extensionsUrl}`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Opening extensions page in your browser: ${extensionsUrl}`,
});
expect(open).toHaveBeenCalledWith(extensionsUrl);
});
@@ -449,13 +419,10 @@ describe('extensionsCommand', () => {
await exploreAction(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `View available extensions at ${extensionsUrl}`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `View available extensions at ${extensionsUrl}`,
});
// Ensure 'open' was not called in the sandbox
expect(open).not.toHaveBeenCalled();
@@ -468,13 +435,10 @@ describe('extensionsCommand', () => {
await exploreAction(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Would open extensions page in your browser: ${extensionsUrl} (skipped in test environment)`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Would open extensions page in your browser: ${extensionsUrl} (skipped in test environment)`,
});
// Ensure 'open' was not called in test environment
expect(open).not.toHaveBeenCalled();
@@ -488,13 +452,10 @@ describe('extensionsCommand', () => {
await exploreAction(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`,
});
});
});
@@ -549,13 +510,10 @@ describe('extensionsCommand', () => {
it('should show usage if no extension name is provided', async () => {
await installAction!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Usage: /extensions install <source>',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Usage: /extensions install <source>',
});
expect(mockInstallExtension).not.toHaveBeenCalled();
});
@@ -572,20 +530,14 @@ describe('extensionsCommand', () => {
source: packageName,
type: 'git',
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Installing extension from "${packageName}"...`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Extension "${packageName}" installed successfully.`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Installing extension from "${packageName}"...`,
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Extension "${packageName}" installed successfully.`,
});
});
it('should show error message on installation failure', async () => {
@@ -603,25 +555,19 @@ describe('extensionsCommand', () => {
source: packageName,
type: 'git',
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: `Failed to install extension from "${packageName}": ${errorMessage}`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: `Failed to install extension from "${packageName}": ${errorMessage}`,
});
});
it('should show error message for invalid source', async () => {
const invalidSource = 'a;b';
await installAction!(mockContext, invalidSource);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: `Invalid source: ${invalidSource}`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: `Invalid source: ${invalidSource}`,
});
expect(mockInstallExtension).not.toHaveBeenCalled();
});
});
@@ -640,13 +586,10 @@ describe('extensionsCommand', () => {
it('should show usage if no extension is provided', async () => {
await linkAction!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Usage: /extensions link <source>',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Usage: /extensions link <source>',
});
expect(mockInstallExtension).not.toHaveBeenCalled();
});
@@ -661,20 +604,14 @@ describe('extensionsCommand', () => {
source: packageName,
type: 'link',
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Linking extension from "${packageName}"...`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Extension "${packageName}" linked successfully.`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Linking extension from "${packageName}"...`,
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Extension "${packageName}" linked successfully.`,
});
});
it('should show error message on linking failure', async () => {
@@ -690,13 +627,10 @@ describe('extensionsCommand', () => {
source: packageName,
type: 'link',
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: `Failed to link extension from "${packageName}": ${errorMessage}`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: `Failed to link extension from "${packageName}": ${errorMessage}`,
});
});
it('should show error message for invalid source', async () => {
@@ -723,13 +657,10 @@ describe('extensionsCommand', () => {
it('should show usage if no extension name is provided', async () => {
await uninstallAction!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Usage: /extensions uninstall <extension-name>',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Usage: /extensions uninstall <extension-name>',
});
expect(mockUninstallExtension).not.toHaveBeenCalled();
});
@@ -737,20 +668,14 @@ describe('extensionsCommand', () => {
const extensionName = 'test-extension';
await uninstallAction!(mockContext, extensionName);
expect(mockUninstallExtension).toHaveBeenCalledWith(extensionName, false);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Uninstalling extension "${extensionName}"...`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: `Extension "${extensionName}" uninstalled successfully.`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Uninstalling extension "${extensionName}"...`,
});
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: `Extension "${extensionName}" uninstalled successfully.`,
});
});
it('should show error message on uninstallation failure', async () => {
@@ -760,13 +685,10 @@ describe('extensionsCommand', () => {
await uninstallAction!(mockContext, extensionName);
expect(mockUninstallExtension).toHaveBeenCalledWith(extensionName, false);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: `Failed to uninstall extension "${extensionName}": ${errorMessage}`,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: `Failed to uninstall extension "${extensionName}": ${errorMessage}`,
});
});
});
@@ -785,13 +707,10 @@ describe('extensionsCommand', () => {
it('should show usage if no extension name is provided', async () => {
await enableAction!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Usage: /extensions enable <extension> [--scope=<user|workspace|session>]',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Usage: /extensions enable <extension> [--scope=<user|workspace|session>]',
});
});
it('should call enableExtension with the provided scope', async () => {
@@ -840,13 +759,10 @@ describe('extensionsCommand', () => {
it('should show usage if no extension name is provided', async () => {
await disableAction!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Usage: /extensions disable <extension> [--scope=<user|workspace|session>]',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Usage: /extensions disable <extension> [--scope=<user|workspace|session>]',
});
});
it('should call disableExtension with the provided scope', async () => {
@@ -912,13 +828,10 @@ describe('extensionsCommand', () => {
await restartAction!(mockContext, '--all');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
});
});
it('restarts all active extensions when --all is provided', async () => {
@@ -939,14 +852,12 @@ describe('extensionsCommand', () => {
type: MessageType.INFO,
text: 'Restarting 2 extensions...',
}),
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: '2 extensions restarted successfully.',
}),
expect.any(Number),
);
expect(mockContext.ui.dispatchExtensionStateUpdate).toHaveBeenCalledWith({
type: 'RESTARTED',
@@ -986,7 +897,6 @@ describe('extensionsCommand', () => {
type: MessageType.ERROR,
text: "Extensions are not yet loaded, can't restart yet",
}),
expect.any(Number),
);
expect(mockRestartExtension).not.toHaveBeenCalled();
});
@@ -999,7 +909,6 @@ describe('extensionsCommand', () => {
type: MessageType.ERROR,
text: 'Usage: /extensions restart <extension-names>|--all',
}),
expect.any(Number),
);
expect(mockRestartExtension).not.toHaveBeenCalled();
});
@@ -1019,7 +928,6 @@ describe('extensionsCommand', () => {
type: MessageType.ERROR,
text: 'Failed to restart some extensions:\n ext1: Failed to restart',
}),
expect.any(Number),
);
});
@@ -1038,7 +946,6 @@ describe('extensionsCommand', () => {
type: MessageType.WARNING,
text: 'Extension(s) not found or not active: ext2',
}),
expect.any(Number),
);
});
@@ -1056,7 +963,6 @@ describe('extensionsCommand', () => {
type: MessageType.WARNING,
text: 'Extension(s) not found or not active: ext2, ext3',
}),
expect.any(Number),
);
});
+138 -236
View File
@@ -37,13 +37,10 @@ function showMessageIfNoExtensions(
extensions: unknown[],
): boolean {
if (extensions.length === 0) {
context.ui.addItem(
{
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: 'No extensions installed. Run `/extensions explore` to check out the gallery.',
});
return true;
}
return false;
@@ -63,7 +60,7 @@ async function listAction(context: CommandContext) {
extensions,
};
context.ui.addItem(historyItem, Date.now());
context.ui.addItem(historyItem);
}
function updateAction(context: CommandContext, args: string): Promise<void> {
@@ -72,13 +69,10 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
const names = all ? null : updateArgs;
if (!all && names?.length === 0) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Usage: /extensions update <extension-names>|--all',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Usage: /extensions update <extension-names>|--all',
});
return Promise.resolve();
}
@@ -103,16 +97,13 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
updateComplete.then((updateInfos) => {
if (updateInfos.length === 0) {
context.ui.addItem(
{
type: MessageType.INFO,
text: 'No extensions to update.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: 'No extensions to update.',
});
}
context.ui.addItem(historyItem, Date.now());
context.ui.addItem(historyItem);
context.ui.setPendingItem(null);
});
@@ -136,26 +127,20 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
(extension) => extension.name === name,
);
if (!extension) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Extension ${name} not found.`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Extension ${name} not found.`,
});
continue;
}
}
}
} catch (error) {
resolveUpdateComplete!([]);
context.ui.addItem(
{
type: MessageType.ERROR,
text: getErrorMessage(error),
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: getErrorMessage(error),
});
}
return updateComplete.then((_) => {});
}
@@ -166,13 +151,10 @@ async function restartAction(
): Promise<void> {
const extensionLoader = context.services.config?.getExtensionLoader();
if (!extensionLoader) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: "Extensions are not yet loaded, can't restart yet",
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: "Extensions are not yet loaded, can't restart yet",
});
return;
}
@@ -185,13 +167,10 @@ async function restartAction(
const all = restartArgs.length === 1 && restartArgs[0] === '--all';
const names = all ? null : restartArgs;
if (!all && names?.length === 0) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Usage: /extensions restart <extension-names>|--all',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Usage: /extensions restart <extension-names>|--all',
});
return Promise.resolve();
}
@@ -208,15 +187,10 @@ async function restartAction(
!extensionsToRestart.some((extension) => extension.name === name),
);
if (notFound.length > 0) {
context.ui.addItem(
{
type: MessageType.WARNING,
text: `Extension(s) not found or not active: ${notFound.join(
', ',
)}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.WARNING,
text: `Extension(s) not found or not active: ${notFound.join(', ')}`,
});
}
}
}
@@ -232,7 +206,7 @@ async function restartAction(
text: `Restarting ${extensionsToRestart.length} extension${s}...`,
color: theme.text.primary,
};
context.ui.addItem(restartingMessage, Date.now());
context.ui.addItem(restartingMessage);
const results = await Promise.allSettled(
extensionsToRestart.map(async (extension) => {
@@ -259,13 +233,10 @@ async function restartAction(
return `${extensionName}: ${getErrorMessage(failure.reason)}`;
})
.join('\n ');
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to restart some extensions:\n ${errorMessages}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to restart some extensions:\n ${errorMessages}`,
});
} else {
const infoItem: HistoryItemInfo = {
type: MessageType.INFO,
@@ -273,7 +244,7 @@ async function restartAction(
icon: emptyIcon,
color: theme.text.primary,
};
context.ui.addItem(infoItem, Date.now());
context.ui.addItem(infoItem);
}
}
@@ -282,42 +253,30 @@ async function exploreAction(context: CommandContext) {
// Only check for NODE_ENV for explicit test mode, not for unit test framework
if (process.env['NODE_ENV'] === 'test') {
context.ui.addItem(
{
type: MessageType.INFO,
text: `Would open extensions page in your browser: ${extensionsUrl} (skipped in test environment)`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Would open extensions page in your browser: ${extensionsUrl} (skipped in test environment)`,
});
} else if (
process.env['SANDBOX'] &&
process.env['SANDBOX'] !== 'sandbox-exec'
) {
context.ui.addItem(
{
type: MessageType.INFO,
text: `View available extensions at ${extensionsUrl}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `View available extensions at ${extensionsUrl}`,
});
} else {
context.ui.addItem(
{
type: MessageType.INFO,
text: `Opening extensions page in your browser: ${extensionsUrl}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Opening extensions page in your browser: ${extensionsUrl}`,
});
try {
await open(extensionsUrl);
} catch (_error) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`,
});
}
}
}
@@ -346,13 +305,10 @@ function getEnableDisableContext(
(parts.length === 3 && parts[1] === '--scope') // --scope <scope>
)
) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Usage: /extensions ${context.invocation?.name} <extension> [--scope=<user|workspace|session>]`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Usage: /extensions ${context.invocation?.name} <extension> [--scope=<user|workspace|session>]`,
});
return null;
}
let scope: SettingScope;
@@ -372,13 +328,10 @@ function getEnableDisableContext(
scope = SettingScope.Session;
break;
default:
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Unsupported scope ${parts[2]}, should be one of "user", "workspace", or "session"`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Unsupported scope ${parts[2]}, should be one of "user", "workspace", or "session"`,
});
debugLogger.error();
return null;
}
@@ -410,13 +363,10 @@ async function disableAction(context: CommandContext, args: string) {
const { names, scope, extensionManager } = enableContext;
for (const name of names) {
await extensionManager.disableExtension(name, scope);
context.ui.addItem(
{
type: MessageType.INFO,
text: `Extension "${name}" disabled for the scope "${scope}"`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Extension "${name}" disabled for the scope "${scope}"`,
});
}
}
@@ -427,13 +377,10 @@ async function enableAction(context: CommandContext, args: string) {
const { names, scope, extensionManager } = enableContext;
for (const name of names) {
await extensionManager.enableExtension(name, scope);
context.ui.addItem(
{
type: MessageType.INFO,
text: `Extension "${name}" enabled for the scope "${scope}"`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Extension "${name}" enabled for the scope "${scope}"`,
});
}
}
@@ -448,13 +395,10 @@ async function installAction(context: CommandContext, args: string) {
const source = args.trim();
if (!source) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Usage: /extensions install <source>`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Usage: /extensions install <source>`,
});
return;
}
@@ -473,45 +417,33 @@ async function installAction(context: CommandContext, args: string) {
}
if (!isValid) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Invalid source: ${source}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Invalid source: ${source}`,
});
return;
}
context.ui.addItem(
{
type: MessageType.INFO,
text: `Installing extension from "${source}"...`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Installing extension from "${source}"...`,
});
try {
const installMetadata = await inferInstallMetadata(source);
const extension =
await extensionLoader.installOrUpdateExtension(installMetadata);
context.ui.addItem(
{
type: MessageType.INFO,
text: `Extension "${extension.name}" installed successfully.`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Extension "${extension.name}" installed successfully.`,
});
} catch (error) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to install extension from "${source}": ${getErrorMessage(
error,
)}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to install extension from "${source}": ${getErrorMessage(
error,
)}`,
});
}
}
@@ -526,49 +458,37 @@ async function linkAction(context: CommandContext, args: string) {
const sourceFilepath = args.trim();
if (!sourceFilepath) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Usage: /extensions link <source>`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Usage: /extensions link <source>`,
});
return;
}
if (/[;&|`'"]/.test(sourceFilepath)) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Source file path contains disallowed characters: ${sourceFilepath}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Source file path contains disallowed characters: ${sourceFilepath}`,
});
return;
}
try {
await stat(sourceFilepath);
} catch (error) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Invalid source: ${sourceFilepath}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Invalid source: ${sourceFilepath}`,
});
debugLogger.error(
`Failed to stat path "${sourceFilepath}": ${getErrorMessage(error)}`,
);
return;
}
context.ui.addItem(
{
type: MessageType.INFO,
text: `Linking extension from "${sourceFilepath}"...`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Linking extension from "${sourceFilepath}"...`,
});
try {
const installMetadata: ExtensionInstallMetadata = {
@@ -577,23 +497,17 @@ async function linkAction(context: CommandContext, args: string) {
};
const extension =
await extensionLoader.installOrUpdateExtension(installMetadata);
context.ui.addItem(
{
type: MessageType.INFO,
text: `Extension "${extension.name}" linked successfully.`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Extension "${extension.name}" linked successfully.`,
});
} catch (error) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to link extension from "${sourceFilepath}": ${getErrorMessage(
error,
)}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to link extension from "${sourceFilepath}": ${getErrorMessage(
error,
)}`,
});
}
}
@@ -608,43 +522,31 @@ async function uninstallAction(context: CommandContext, args: string) {
const name = args.trim();
if (!name) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Usage: /extensions uninstall <extension-name>`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Usage: /extensions uninstall <extension-name>`,
});
return;
}
context.ui.addItem(
{
type: MessageType.INFO,
text: `Uninstalling extension "${name}"...`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Uninstalling extension "${name}"...`,
});
try {
await extensionLoader.uninstallExtension(name, false);
context.ui.addItem(
{
type: MessageType.INFO,
text: `Extension "${name}" uninstalled successfully.`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: `Extension "${name}" uninstalled successfully.`,
});
} catch (error) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to uninstall extension "${name}": ${getErrorMessage(
error,
)}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to uninstall extension "${name}": ${getErrorMessage(
error,
)}`,
});
}
}
@@ -40,7 +40,6 @@ describe('helpCommand', () => {
type: MessageType.HELP,
timestamp: expect.any(Date),
}),
expect.any(Number),
);
});
+1 -1
View File
@@ -20,6 +20,6 @@ export const helpCommand: SlashCommand = {
timestamp: new Date(),
};
context.ui.addItem(helpItem, Date.now());
context.ui.addItem(helpItem);
},
};
@@ -109,7 +109,6 @@ describe('hooksCommand', () => {
expect.objectContaining({
type: MessageType.HOOKS_LIST,
}),
expect.any(Number),
);
});
});
@@ -155,7 +154,6 @@ describe('hooksCommand', () => {
type: MessageType.HOOKS_LIST,
hooks: [],
}),
expect.any(Number),
);
});
@@ -179,7 +177,6 @@ describe('hooksCommand', () => {
type: MessageType.HOOKS_LIST,
hooks: [],
}),
expect.any(Number),
);
});
@@ -208,7 +205,6 @@ describe('hooksCommand', () => {
type: MessageType.HOOKS_LIST,
hooks: mockHooks,
}),
expect.any(Number),
);
});
});
+1 -1
View File
@@ -37,7 +37,7 @@ async function panelAction(
hooks: allHooks,
};
context.ui.addItem(hooksListItem, Date.now());
context.ui.addItem(hooksListItem);
}
/**
@@ -231,7 +231,6 @@ describe('mcpCommand', () => {
}),
]),
}),
expect.any(Number),
);
});
@@ -246,7 +245,6 @@ describe('mcpCommand', () => {
type: MessageType.MCP_STATUS,
showDescriptions: true,
}),
expect.any(Number),
);
});
@@ -261,7 +259,6 @@ describe('mcpCommand', () => {
type: MessageType.MCP_STATUS,
showDescriptions: false,
}),
expect.any(Number),
);
});
});
+18 -30
View File
@@ -91,19 +91,16 @@ const authCommand: SlashCommand = {
// The authentication process will discover OAuth requirements automatically
const displayListener = (message: string) => {
context.ui.addItem({ type: 'info', text: message }, Date.now());
context.ui.addItem({ type: 'info', text: message });
};
appEvents.on(AppEvent.OauthDisplayMessage, displayListener);
try {
context.ui.addItem(
{
type: 'info',
text: `Starting OAuth authentication for MCP server '${serverName}'...`,
},
Date.now(),
);
context.ui.addItem({
type: 'info',
text: `Starting OAuth authentication for MCP server '${serverName}'...`,
});
// Import dynamically to avoid circular dependencies
const { MCPOAuthProvider } = await import('@google/gemini-cli-core');
@@ -122,24 +119,18 @@ const authCommand: SlashCommand = {
appEvents,
);
context.ui.addItem(
{
type: 'info',
text: `✅ Successfully authenticated with MCP server '${serverName}'!`,
},
Date.now(),
);
context.ui.addItem({
type: 'info',
text: `✅ Successfully authenticated with MCP server '${serverName}'!`,
});
// Trigger tool re-discovery to pick up authenticated server
const mcpClientManager = config.getMcpClientManager();
if (mcpClientManager) {
context.ui.addItem(
{
type: 'info',
text: `Restarting MCP server '${serverName}'...`,
},
Date.now(),
);
context.ui.addItem({
type: 'info',
text: `Restarting MCP server '${serverName}'...`,
});
await mcpClientManager.restartServer(serverName);
}
// Update the client with the new tools
@@ -279,7 +270,7 @@ const listAction = async (
showSchema,
};
context.ui.addItem(mcpStatusItem, Date.now());
context.ui.addItem(mcpStatusItem);
};
const listCommand: SlashCommand = {
@@ -335,13 +326,10 @@ const refreshCommand: SlashCommand = {
};
}
context.ui.addItem(
{
type: 'info',
text: 'Restarting MCP servers...',
},
Date.now(),
);
context.ui.addItem({
type: 'info',
text: 'Restarting MCP servers...',
});
await mcpClientManager.restart();
@@ -91,7 +91,6 @@ describe('skillsCommand', () => {
],
showDescriptions: true,
}),
expect.any(Number),
);
});
@@ -120,7 +119,6 @@ describe('skillsCommand', () => {
],
showDescriptions: true,
}),
expect.any(Number),
);
});
@@ -132,7 +130,6 @@ describe('skillsCommand', () => {
expect.objectContaining({
showDescriptions: false,
}),
expect.any(Number),
);
});
@@ -229,7 +226,6 @@ describe('skillsCommand', () => {
type: MessageType.INFO,
text: 'Skill "skill1" disabled by adding it to the disabled list in project (/workspace) settings. Use "/skills reload" for it to take effect.',
}),
expect.any(Number),
);
});
@@ -258,7 +254,6 @@ describe('skillsCommand', () => {
type: MessageType.INFO,
text: 'Skill "skill1" enabled by removing it from the disabled list in project (/workspace) and user (/user/settings.json) settings. Use "/skills reload" for it to take effect.',
}),
expect.any(Number),
);
});
@@ -298,7 +293,6 @@ describe('skillsCommand', () => {
type: MessageType.INFO,
text: 'Skill "skill1" enabled by removing it from the disabled list in project (/workspace) and user (/user/settings.json) settings. Use "/skills reload" for it to take effect.',
}),
expect.any(Number),
);
});
@@ -313,7 +307,6 @@ describe('skillsCommand', () => {
type: MessageType.ERROR,
text: 'Skill "non-existent" not found.',
}),
expect.any(Number),
);
});
});
@@ -359,7 +352,6 @@ describe('skillsCommand', () => {
type: MessageType.INFO,
text: 'Agent skills reloaded successfully.',
}),
expect.any(Number),
);
});
@@ -385,7 +377,6 @@ describe('skillsCommand', () => {
type: MessageType.INFO,
text: 'Agent skills reloaded successfully. 1 newly available skill.',
}),
expect.any(Number),
);
});
@@ -409,7 +400,6 @@ describe('skillsCommand', () => {
type: MessageType.INFO,
text: 'Agent skills reloaded successfully. 1 skill no longer available.',
}),
expect.any(Number),
);
});
@@ -434,7 +424,6 @@ describe('skillsCommand', () => {
type: MessageType.INFO,
text: 'Agent skills reloaded successfully. 1 newly available skill and 1 skill no longer available.',
}),
expect.any(Number),
);
});
@@ -451,7 +440,6 @@ describe('skillsCommand', () => {
type: MessageType.ERROR,
text: 'Could not retrieve configuration.',
}),
expect.any(Number),
);
});
@@ -477,7 +465,6 @@ describe('skillsCommand', () => {
type: MessageType.ERROR,
text: 'Failed to reload skills: Reload failed',
}),
expect.any(Number),
);
});
});
+39 -66
View File
@@ -39,13 +39,10 @@ async function listAction(
const skillManager = context.services.config?.getSkillManager();
if (!skillManager) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Could not retrieve skill manager.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Could not retrieve skill manager.',
});
return;
}
@@ -66,7 +63,7 @@ async function listAction(
showDescriptions: useShowDescriptions,
};
context.ui.addItem(skillsListItem, Date.now());
context.ui.addItem(skillsListItem);
}
async function disableAction(
@@ -75,25 +72,19 @@ async function disableAction(
): Promise<void | SlashCommandActionReturn> {
const skillName = args.trim();
if (!skillName) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Please provide a skill name to disable.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Please provide a skill name to disable.',
});
return;
}
const skillManager = context.services.config?.getSkillManager();
const skill = skillManager?.getSkill(skillName);
if (!skill) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Skill "${skillName}" not found.`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Skill "${skillName}" not found.`,
});
return;
}
@@ -111,13 +102,10 @@ async function disableAction(
feedback += ' Use "/skills reload" for it to take effect.';
}
context.ui.addItem(
{
type: MessageType.INFO,
text: feedback,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: feedback,
});
}
async function enableAction(
@@ -126,13 +114,10 @@ async function enableAction(
): Promise<void | SlashCommandActionReturn> {
const skillName = args.trim();
if (!skillName) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Please provide a skill name to enable.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Please provide a skill name to enable.',
});
return;
}
@@ -146,13 +131,10 @@ async function enableAction(
feedback += ' Use "/skills reload" for it to take effect.';
}
context.ui.addItem(
{
type: MessageType.INFO,
text: feedback,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.INFO,
text: feedback,
});
}
async function reloadAction(
@@ -160,13 +142,10 @@ async function reloadAction(
): Promise<void | SlashCommandActionReturn> {
const config = context.services.config;
if (!config) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Could not retrieve configuration.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Could not retrieve configuration.',
});
return;
}
@@ -226,27 +205,21 @@ async function reloadAction(
successText += ` ${details.join(' and ')}.`;
}
context.ui.addItem(
{
type: 'info',
text: successText,
icon: '',
color: 'green',
} as HistoryItemInfo,
Date.now(),
);
context.ui.addItem({
type: 'info',
text: successText,
icon: '✓ ',
color: 'green',
} as HistoryItemInfo);
} catch (error) {
clearTimeout(pendingTimeout);
if (pendingItemSet) {
context.ui.setPendingItem(null);
}
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to reload skills: ${error instanceof Error ? error.message : String(error)}`,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to reload skills: ${error instanceof Error ? error.message : String(error)}`,
});
}
}
@@ -37,13 +37,10 @@ describe('statsCommand', () => {
const expectedDuration = formatDuration(
endTime.getTime() - startTime.getTime(),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.STATS,
duration: expectedDuration,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.STATS,
duration: expectedDuration,
});
});
it('should fetch and display quota if config is available', async () => {
@@ -62,7 +59,6 @@ describe('statsCommand', () => {
expect.objectContaining({
quotas: mockQuota,
}),
expect.any(Number),
);
});
@@ -75,12 +71,9 @@ describe('statsCommand', () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
modelSubCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.MODEL_STATS,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.MODEL_STATS,
});
});
it('should display tool stats when using the "tools" subcommand', () => {
@@ -92,11 +85,8 @@ describe('statsCommand', () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
toolsSubCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.TOOL_STATS,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.TOOL_STATS,
});
});
});
+11 -20
View File
@@ -17,13 +17,10 @@ async function defaultSessionView(context: CommandContext) {
const now = new Date();
const { sessionStartTime } = context.session.stats;
if (!sessionStartTime) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Session start time is unavailable, cannot calculate stats.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Session start time is unavailable, cannot calculate stats.',
});
return;
}
const wallDuration = now.getTime() - sessionStartTime.getTime();
@@ -40,7 +37,7 @@ async function defaultSessionView(context: CommandContext) {
}
}
context.ui.addItem(statsItem, Date.now());
context.ui.addItem(statsItem);
}
export const statsCommand: SlashCommand = {
@@ -68,12 +65,9 @@ export const statsCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: (context: CommandContext) => {
context.ui.addItem(
{
type: MessageType.MODEL_STATS,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.MODEL_STATS,
});
},
},
{
@@ -82,12 +76,9 @@ export const statsCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: (context: CommandContext) => {
context.ui.addItem(
{
type: MessageType.TOOL_STATS,
},
Date.now(),
);
context.ui.addItem({
type: MessageType.TOOL_STATS,
});
},
},
],
@@ -40,13 +40,10 @@ describe('toolsCommand', () => {
if (!toolsCommand.action) throw new Error('Action not defined');
await toolsCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Could not retrieve tool registry.',
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Could not retrieve tool registry.',
});
});
it('should display "No tools available" when none are found', async () => {
@@ -63,14 +60,11 @@ describe('toolsCommand', () => {
if (!toolsCommand.action) throw new Error('Action not defined');
await toolsCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.TOOLS_LIST,
tools: [],
showDescriptions: false,
},
expect.any(Number),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.TOOLS_LIST,
tools: [],
showDescriptions: false,
});
});
it('should list tools without descriptions by default', async () => {
+5 -8
View File
@@ -27,13 +27,10 @@ export const toolsCommand: SlashCommand = {
const toolRegistry = context.services.config?.getToolRegistry();
if (!toolRegistry) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Could not retrieve tool registry.',
},
Date.now(),
);
context.ui.addItem({
type: MessageType.ERROR,
text: 'Could not retrieve tool registry.',
});
return;
}
@@ -51,6 +48,6 @@ export const toolsCommand: SlashCommand = {
showDescriptions: useShowDescriptions,
};
context.ui.addItem(toolsListItem, Date.now());
context.ui.addItem(toolsListItem);
},
};
@@ -11,7 +11,7 @@ import {
MultiFolderTrustChoice,
type MultiFolderTrustDialogProps,
} from './MultiFolderTrustDialog.js';
import { vi } from 'vitest';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import {
TrustLevel,
type LoadedTrustedFolders,
@@ -213,13 +213,10 @@ describe('MultiFolderTrustDialog', () => {
onSelect(MultiFolderTrustChoice.YES);
});
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Configuration is not available.',
},
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
expect(mockOnComplete).toHaveBeenCalled();
expect(mockFinishAddingDirectories).not.toHaveBeenCalled();
});
@@ -31,13 +31,16 @@ export interface MultiFolderTrustDialogProps {
config: Config,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp: number,
baseTimestamp?: number,
) => number,
added: string[],
errors: string[],
) => Promise<void>;
config: Config;
addItem: (itemData: Omit<HistoryItem, 'id'>, baseTimestamp: number) => number;
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
) => number;
}
export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
@@ -95,13 +98,10 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
setSubmitted(true);
if (!config) {
addItem(
{
type: MessageType.ERROR,
text: 'Configuration is not available.',
},
Date.now(),
);
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
onComplete();
return;
}
@@ -780,7 +780,6 @@ describe('useGeminiStream', () => {
'Agent execution stopped: Stop reason from hook',
),
}),
expect.any(Number),
);
// Ensure we do NOT call back to the API
expect(mockSendMessageStream).not.toHaveBeenCalled();
@@ -1085,13 +1084,10 @@ describe('useGeminiStream', () => {
// Verify cancellation message is added
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: 'Request cancelled.',
},
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith({
type: MessageType.INFO,
text: 'Request cancelled.',
});
});
// Verify state is reset
@@ -1194,7 +1190,6 @@ describe('useGeminiStream', () => {
expect.objectContaining({
text: 'Request cancelled.',
}),
expect.any(Number),
);
});
@@ -1330,7 +1325,6 @@ describe('useGeminiStream', () => {
expect.objectContaining({
text: 'Request cancelled.',
}),
expect.any(Number),
);
});
@@ -1995,13 +1989,10 @@ describe('useGeminiStream', () => {
});
await waitFor(() => {
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
text: expectedMessage,
},
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith({
type: 'info',
text: expectedMessage,
});
});
},
);
@@ -2644,13 +2635,10 @@ describe('useGeminiStream', () => {
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
// Verify appropriate message was added
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Loop detection has been disabled for this session. Retrying request...',
},
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith({
type: 'info',
text: 'Loop detection has been disabled for this session. Retrying request...',
});
// Verify that the request was retried
await waitFor(() => {
@@ -2707,13 +2695,10 @@ describe('useGeminiStream', () => {
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
// Verify appropriate message was added
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
},
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith({
type: 'info',
text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
});
// Verify that the request was NOT retried
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
@@ -2750,13 +2735,10 @@ describe('useGeminiStream', () => {
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
// Verify first message was added
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
},
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith({
type: 'info',
text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
});
// Second loop detection - set up fresh mock for second call
mockSendMessageStream.mockReturnValueOnce(
@@ -2800,13 +2782,10 @@ describe('useGeminiStream', () => {
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
// Verify second message was added
expect(mockAddItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Loop detection has been disabled for this session. Retrying request...',
},
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith({
type: 'info',
text: 'Loop detection has been disabled for this session. Retrying request...',
});
// Verify that the request was retried
await waitFor(() => {
+41 -72
View File
@@ -149,7 +149,6 @@ export const useGeminiStream = (
mapTrackedToolCallsToDisplay(
completedToolCallsFromScheduler as TrackedToolCall[],
),
Date.now(),
);
// Clear the live-updating display now that the final state is in history.
@@ -248,10 +247,7 @@ export const useGeminiStream = (
prevActiveShellPtyIdRef.current !== null &&
activeShellPtyId === null
) {
addItem(
{ type: MessageType.INFO, text: 'Request cancelled.' },
Date.now(),
);
addItem({ type: MessageType.INFO, text: 'Request cancelled.' });
setIsResponding(false);
}
prevActiveShellPtyIdRef.current = activeShellPtyId;
@@ -351,12 +347,9 @@ export const useGeminiStream = (
}
return tool;
});
addItem(
{ ...toolGroup, tools: updatedTools } as HistoryItemWithoutId,
Date.now(),
);
addItem({ ...toolGroup, tools: updatedTools } as HistoryItemWithoutId);
} else {
addItem(pendingHistoryItemRef.current, Date.now());
addItem(pendingHistoryItemRef.current);
}
}
setPendingHistoryItem(null);
@@ -368,13 +361,10 @@ export const useGeminiStream = (
// If shell is active, we delay this message to ensure correct ordering
// (Shell item first, then Info message).
if (!activeShellPtyId) {
addItem(
{
type: MessageType.INFO,
text: 'Request cancelled.',
},
Date.now(),
);
addItem({
type: MessageType.INFO,
text: 'Request cancelled.',
});
setIsResponding(false);
}
}
@@ -719,32 +709,26 @@ export const useGeminiStream = (
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
return addItem(
{
type: 'info',
text:
`IMPORTANT: This conversation exceeded the compress threshold. ` +
`A compressed context will be sent for future messages (compressed from: ` +
`${eventValue?.originalTokenCount ?? 'unknown'} to ` +
`${eventValue?.newTokenCount ?? 'unknown'} tokens).`,
},
Date.now(),
);
return addItem({
type: 'info',
text:
`IMPORTANT: This conversation exceeded the compress threshold. ` +
`A compressed context will be sent for future messages (compressed from: ` +
`${eventValue?.originalTokenCount ?? 'unknown'} to ` +
`${eventValue?.newTokenCount ?? 'unknown'} tokens).`,
});
},
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
);
const handleMaxSessionTurnsEvent = useCallback(
() =>
addItem(
{
type: 'info',
text:
`The session has reached the maximum number of turns: ${config.getMaxSessionTurns()}. ` +
`Please update this limit in your setting.json file.`,
},
Date.now(),
),
addItem({
type: 'info',
text:
`The session has reached the maximum number of turns: ${config.getMaxSessionTurns()}. ` +
`Please update this limit in your setting.json file.`,
}),
[addItem, config],
);
@@ -764,13 +748,10 @@ export const useGeminiStream = (
' Please try reducing the size of your message or use the `/compress` command to compress the chat history.';
}
addItem(
{
type: 'info',
text,
},
Date.now(),
);
addItem({
type: 'info',
text,
});
},
[addItem, onCancelSubmit, config],
);
@@ -1041,13 +1022,10 @@ export const useGeminiStream = (
.getGeminiClient()
.getLoopDetectionService()
.disableForSession();
addItem(
{
type: 'info',
text: `Loop detection has been disabled for this session. Retrying request...`,
},
Date.now(),
);
addItem({
type: 'info',
text: `Loop detection has been disabled for this session. Retrying request...`,
});
if (lastQueryRef.current && lastPromptIdRef.current) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -1058,13 +1036,10 @@ export const useGeminiStream = (
);
}
} else {
addItem(
{
type: 'info',
text: `A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.`,
},
Date.now(),
);
addItem({
type: 'info',
text: `A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.`,
});
}
},
});
@@ -1215,13 +1190,10 @@ export const useGeminiStream = (
);
if (stopExecutionTool && stopExecutionTool.response.error) {
addItem(
{
type: MessageType.INFO,
text: `Agent execution stopped: ${stopExecutionTool.response.error.message}`,
},
Date.now(),
);
addItem({
type: MessageType.INFO,
text: `Agent execution stopped: ${stopExecutionTool.response.error.message}`,
});
setIsResponding(false);
const callIdsToMarkAsSubmitted = geminiTools.map(
@@ -1240,13 +1212,10 @@ export const useGeminiStream = (
// If the turn was cancelled via the imperative escape key flow,
// the cancellation message is added there. We check the ref to avoid duplication.
if (!turnCancelledRef.current) {
addItem(
{
type: MessageType.INFO,
text: 'Request cancelled.',
},
Date.now(),
);
addItem({
type: MessageType.INFO,
text: 'Request cancelled.',
});
}
setIsResponding(false);
@@ -200,4 +200,23 @@ describe('useHistoryManager', () => {
expect(result.current.history[1].text).toBe('Gemini response');
expect(result.current.history[2].text).toBe('Message 1');
});
it('should use Date.now() as default baseTimestamp if not provided', () => {
const { result } = renderHook(() => useHistory());
const before = Date.now();
const itemData: Omit<HistoryItem, 'id'> = {
type: 'user',
text: 'Default timestamp test',
};
act(() => {
result.current.addItem(itemData);
});
const after = Date.now();
expect(result.current.history).toHaveLength(1);
// ID should be >= before + 1 (since counter starts at 0 and increments to 1)
expect(result.current.history[0].id).toBeGreaterThanOrEqual(before + 1);
expect(result.current.history[0].id).toBeLessThanOrEqual(after + 1);
});
});
@@ -17,7 +17,7 @@ export interface UseHistoryManagerReturn {
history: HistoryItem[];
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp: number,
baseTimestamp?: number,
isResuming?: boolean,
) => number; // Returns the generated ID
updateItem: (
@@ -56,7 +56,7 @@ export function useHistory({
const addItem = useCallback(
(
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp: number,
baseTimestamp: number = Date.now(),
isResuming: boolean = false,
): number => {
const id = getNextMessageId(baseTimestamp);
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { Mock } from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
@@ -132,7 +132,6 @@ describe('useIncludeDirsTrust', () => {
expect.objectContaining({
text: expect.stringContaining("Error adding '/dir2': Test error"),
}),
expect.any(Number),
);
expect(
mockConfig.clearPendingIncludeDirectories,
@@ -18,18 +18,18 @@ import { MessageType, type HistoryItem } from '../types.js';
async function finishAddingDirectories(
config: Config,
addItem: (itemData: Omit<HistoryItem, 'id'>, baseTimestamp: number) => number,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
) => number,
added: string[],
errors: string[],
) {
if (!config) {
addItem(
{
type: MessageType.ERROR,
text: 'Configuration is not available.',
},
Date.now(),
);
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
});
return;
}
@@ -49,7 +49,7 @@ async function finishAddingDirectories(
}
if (errors.length > 0) {
addItem({ type: MessageType.ERROR, text: errors.join('\n') }, Date.now());
addItem({ type: MessageType.ERROR, text: errors.join('\n') });
}
}