mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
110
packages/cli/src/ui/components/views/ExtensionsList.test.tsx
Normal file
110
packages/cli/src/ui/components/views/ExtensionsList.test.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { vi } from 'vitest';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { ExtensionUpdateState } from '../../state/extensions.js';
|
||||
import { ExtensionsList } from './ExtensionsList.js';
|
||||
import { createMockCommandContext } from '../../../test-utils/mockCommandContext.js';
|
||||
|
||||
vi.mock('../../contexts/UIStateContext.js');
|
||||
|
||||
const mockUseUIState = vi.mocked(useUIState);
|
||||
|
||||
const mockExtensions = [
|
||||
{ name: 'ext-one', version: '1.0.0', isActive: true },
|
||||
{ name: 'ext-two', version: '2.1.0', isActive: true },
|
||||
{ name: 'ext-disabled', version: '3.0.0', isActive: false },
|
||||
];
|
||||
|
||||
describe('<ExtensionsList />', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
const mockUIState = (
|
||||
extensions: unknown[],
|
||||
extensionsUpdateState: Map<string, ExtensionUpdateState>,
|
||||
disabledExtensions: string[] = [],
|
||||
) => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
commandContext: createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getExtensions: () => extensions,
|
||||
},
|
||||
settings: {
|
||||
merged: {
|
||||
extensions: {
|
||||
disabled: disabledExtensions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
extensionsUpdateState,
|
||||
// Add other required properties from UIState if needed by the component
|
||||
} as never);
|
||||
};
|
||||
|
||||
it('should render "No extensions installed." if there are no extensions', () => {
|
||||
mockUIState([], new Map());
|
||||
const { lastFrame } = render(<ExtensionsList />);
|
||||
expect(lastFrame()).toContain('No extensions installed.');
|
||||
});
|
||||
|
||||
it('should render a list of extensions with their version and status', () => {
|
||||
mockUIState(mockExtensions, new Map(), ['ext-disabled']);
|
||||
const { lastFrame } = render(<ExtensionsList />);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('ext-one (v1.0.0) - active');
|
||||
expect(output).toContain('ext-two (v2.1.0) - active');
|
||||
expect(output).toContain('ext-disabled (v3.0.0) - disabled');
|
||||
});
|
||||
|
||||
it('should display "unknown state" if an extension has no update state', () => {
|
||||
mockUIState([mockExtensions[0]], new Map());
|
||||
const { lastFrame } = render(<ExtensionsList />);
|
||||
expect(lastFrame()).toContain('(unknown state)');
|
||||
});
|
||||
|
||||
const stateTestCases = [
|
||||
{
|
||||
state: ExtensionUpdateState.CHECKING_FOR_UPDATES,
|
||||
expectedText: '(checking for updates)',
|
||||
},
|
||||
{
|
||||
state: ExtensionUpdateState.UPDATING,
|
||||
expectedText: '(updating)',
|
||||
},
|
||||
{
|
||||
state: ExtensionUpdateState.UPDATE_AVAILABLE,
|
||||
expectedText: '(update available)',
|
||||
},
|
||||
{
|
||||
state: ExtensionUpdateState.UPDATED_NEEDS_RESTART,
|
||||
expectedText: '(updated, needs restart)',
|
||||
},
|
||||
{
|
||||
state: ExtensionUpdateState.ERROR,
|
||||
expectedText: '(error)',
|
||||
},
|
||||
{
|
||||
state: ExtensionUpdateState.UP_TO_DATE,
|
||||
expectedText: '(up to date)',
|
||||
},
|
||||
];
|
||||
|
||||
for (const { state, expectedText } of stateTestCases) {
|
||||
it(`should correctly display the state: ${state}`, () => {
|
||||
const updateState = new Map([[mockExtensions[0].name, state]]);
|
||||
mockUIState([mockExtensions[0]], updateState);
|
||||
const { lastFrame } = render(<ExtensionsList />);
|
||||
expect(lastFrame()).toContain(expectedText);
|
||||
});
|
||||
}
|
||||
});
|
||||
67
packages/cli/src/ui/components/views/ExtensionsList.tsx
Normal file
67
packages/cli/src/ui/components/views/ExtensionsList.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { ExtensionUpdateState } from '../../state/extensions.js';
|
||||
|
||||
export const ExtensionsList = () => {
|
||||
const { commandContext, extensionsUpdateState } = useUIState();
|
||||
const allExtensions = commandContext.services.config!.getExtensions();
|
||||
const settings = commandContext.services.settings;
|
||||
const disabledExtensions = settings.merged.extensions?.disabled ?? [];
|
||||
|
||||
if (allExtensions.length === 0) {
|
||||
return <Text>No extensions installed.</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1} marginBottom={1}>
|
||||
<Text>Installed extensions:</Text>
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
{allExtensions.map((ext) => {
|
||||
const state = extensionsUpdateState.get(ext.name);
|
||||
const isActive = !disabledExtensions.includes(ext.name);
|
||||
const activeString = isActive ? 'active' : 'disabled';
|
||||
|
||||
let stateColor = 'gray';
|
||||
const stateText = state || 'unknown state';
|
||||
|
||||
switch (state) {
|
||||
case ExtensionUpdateState.CHECKING_FOR_UPDATES:
|
||||
case ExtensionUpdateState.UPDATING:
|
||||
stateColor = 'cyan';
|
||||
break;
|
||||
case ExtensionUpdateState.UPDATE_AVAILABLE:
|
||||
case ExtensionUpdateState.UPDATED_NEEDS_RESTART:
|
||||
stateColor = 'yellow';
|
||||
break;
|
||||
case ExtensionUpdateState.ERROR:
|
||||
stateColor = 'red';
|
||||
break;
|
||||
case ExtensionUpdateState.UP_TO_DATE:
|
||||
case ExtensionUpdateState.NOT_UPDATABLE:
|
||||
stateColor = 'green';
|
||||
break;
|
||||
default:
|
||||
console.error(`Unhandled ExtensionUpdateState ${state}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={ext.name}>
|
||||
<Text>
|
||||
<Text color="cyan">{`${ext.name} (v${ext.version})`}</Text>
|
||||
{` - ${activeString}`}
|
||||
{<Text color={stateColor}>{` (${stateText})`}</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
163
packages/cli/src/ui/components/views/McpStatus.test.tsx
Normal file
163
packages/cli/src/ui/components/views/McpStatus.test.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { McpStatus } from './McpStatus.js';
|
||||
import { MCPServerStatus } from '@qwen-code/qwen-code-core';
|
||||
import { MessageType } from '../../types.js';
|
||||
|
||||
describe('McpStatus', () => {
|
||||
const baseProps = {
|
||||
type: MessageType.MCP_STATUS,
|
||||
servers: {
|
||||
'server-1': {
|
||||
url: 'http://localhost:8080',
|
||||
name: 'server-1',
|
||||
description: 'A test server',
|
||||
},
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
serverName: 'server-1',
|
||||
name: 'tool-1',
|
||||
description: 'A test tool',
|
||||
schema: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
prompts: [],
|
||||
blockedServers: [],
|
||||
serverStatus: () => MCPServerStatus.CONNECTED,
|
||||
authStatus: {},
|
||||
discoveryInProgress: false,
|
||||
connectingServers: [],
|
||||
showDescriptions: true,
|
||||
showSchema: false,
|
||||
showTips: false,
|
||||
};
|
||||
|
||||
it('renders correctly with a connected server', () => {
|
||||
const { lastFrame } = render(<McpStatus {...baseProps} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with authenticated OAuth status', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus {...baseProps} authStatus={{ 'server-1': 'authenticated' }} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with expired OAuth status', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus {...baseProps} authStatus={{ 'server-1': 'expired' }} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with unauthenticated OAuth status', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
authStatus={{ 'server-1': 'unauthenticated' }}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with a disconnected server', async () => {
|
||||
vi.spyOn(
|
||||
await import('@qwen-code/qwen-code-core'),
|
||||
'getMCPServerStatus',
|
||||
).mockReturnValue(MCPServerStatus.DISCONNECTED);
|
||||
const { lastFrame } = render(<McpStatus {...baseProps} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly when discovery is in progress', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus {...baseProps} discoveryInProgress={true} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with schema enabled', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus {...baseProps} showSchema={true} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with parametersJsonSchema', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
tools={[
|
||||
{
|
||||
serverName: 'server-1',
|
||||
name: 'tool-1',
|
||||
description: 'A test tool',
|
||||
schema: {
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
showSchema={true}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with tips enabled', () => {
|
||||
const { lastFrame } = render(<McpStatus {...baseProps} showTips={true} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with prompts', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
prompts={[
|
||||
{
|
||||
serverName: 'server-1',
|
||||
name: 'prompt-1',
|
||||
description: 'A test prompt',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with a blocked server', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus
|
||||
{...baseProps}
|
||||
blockedServers={[{ name: 'server-1', extensionName: 'test-extension' }]}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with a connecting server', () => {
|
||||
const { lastFrame } = render(
|
||||
<McpStatus {...baseProps} connectingServers={['server-1']} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
281
packages/cli/src/ui/components/views/McpStatus.tsx
Normal file
281
packages/cli/src/ui/components/views/McpStatus.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { MCPServerConfig } from '@qwen-code/qwen-code-core';
|
||||
import { MCPServerStatus } from '@qwen-code/qwen-code-core';
|
||||
import { Box, Text } from 'ink';
|
||||
import type React from 'react';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type {
|
||||
HistoryItemMcpStatus,
|
||||
JsonMcpPrompt,
|
||||
JsonMcpTool,
|
||||
} from '../../types.js';
|
||||
|
||||
interface McpStatusProps {
|
||||
servers: Record<string, MCPServerConfig>;
|
||||
tools: JsonMcpTool[];
|
||||
prompts: JsonMcpPrompt[];
|
||||
blockedServers: Array<{ name: string; extensionName: string }>;
|
||||
serverStatus: (serverName: string) => MCPServerStatus;
|
||||
authStatus: HistoryItemMcpStatus['authStatus'];
|
||||
discoveryInProgress: boolean;
|
||||
connectingServers: string[];
|
||||
showDescriptions: boolean;
|
||||
showSchema: boolean;
|
||||
showTips: boolean;
|
||||
}
|
||||
|
||||
export const McpStatus: React.FC<McpStatusProps> = ({
|
||||
servers,
|
||||
tools,
|
||||
prompts,
|
||||
blockedServers,
|
||||
serverStatus,
|
||||
authStatus,
|
||||
discoveryInProgress,
|
||||
connectingServers,
|
||||
showDescriptions,
|
||||
showSchema,
|
||||
showTips,
|
||||
}) => {
|
||||
const serverNames = Object.keys(servers);
|
||||
|
||||
if (serverNames.length === 0 && blockedServers.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>No MCP servers configured.</Text>
|
||||
<Text>
|
||||
Please view MCP documentation in your browser:{' '}
|
||||
<Text color={theme.text.link}>
|
||||
https://goo.gle/gemini-cli-docs-mcp
|
||||
</Text>{' '}
|
||||
or use the cli /docs command
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{discoveryInProgress && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={theme.status.warning}>
|
||||
⏳ MCP servers are starting up ({connectingServers.length}{' '}
|
||||
initializing)...
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
Note: First startup may take longer. Tool availability will update
|
||||
automatically.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Text bold>Configured MCP servers:</Text>
|
||||
<Box height={1} />
|
||||
|
||||
{serverNames.map((serverName) => {
|
||||
const server = servers[serverName];
|
||||
const serverTools = tools.filter(
|
||||
(tool) => tool.serverName === serverName,
|
||||
);
|
||||
const serverPrompts = prompts.filter(
|
||||
(prompt) => prompt.serverName === serverName,
|
||||
);
|
||||
const originalStatus = serverStatus(serverName);
|
||||
const hasCachedItems =
|
||||
serverTools.length > 0 || serverPrompts.length > 0;
|
||||
const status =
|
||||
originalStatus === MCPServerStatus.DISCONNECTED && hasCachedItems
|
||||
? MCPServerStatus.CONNECTED
|
||||
: originalStatus;
|
||||
|
||||
let statusIndicator = '';
|
||||
let statusText = '';
|
||||
let statusColor = theme.text.primary;
|
||||
|
||||
switch (status) {
|
||||
case MCPServerStatus.CONNECTED:
|
||||
statusIndicator = '🟢';
|
||||
statusText = 'Ready';
|
||||
statusColor = theme.status.success;
|
||||
break;
|
||||
case MCPServerStatus.CONNECTING:
|
||||
statusIndicator = '🔄';
|
||||
statusText = 'Starting... (first startup may take longer)';
|
||||
statusColor = theme.status.warning;
|
||||
break;
|
||||
case MCPServerStatus.DISCONNECTED:
|
||||
default:
|
||||
statusIndicator = '🔴';
|
||||
statusText = 'Disconnected';
|
||||
statusColor = theme.status.error;
|
||||
break;
|
||||
}
|
||||
|
||||
let serverDisplayName = serverName;
|
||||
if (server.extensionName) {
|
||||
serverDisplayName += ` (from ${server.extensionName})`;
|
||||
}
|
||||
|
||||
const toolCount = serverTools.length;
|
||||
const promptCount = serverPrompts.length;
|
||||
const parts = [];
|
||||
if (toolCount > 0) {
|
||||
parts.push(`${toolCount} ${toolCount === 1 ? 'tool' : 'tools'}`);
|
||||
}
|
||||
if (promptCount > 0) {
|
||||
parts.push(
|
||||
`${promptCount} ${promptCount === 1 ? 'prompt' : 'prompts'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const serverAuthStatus = authStatus[serverName];
|
||||
let authStatusNode: React.ReactNode = null;
|
||||
if (serverAuthStatus === 'authenticated') {
|
||||
authStatusNode = <Text> (OAuth)</Text>;
|
||||
} else if (serverAuthStatus === 'expired') {
|
||||
authStatusNode = (
|
||||
<Text color={theme.status.error}> (OAuth expired)</Text>
|
||||
);
|
||||
} else if (serverAuthStatus === 'unauthenticated') {
|
||||
authStatusNode = (
|
||||
<Text color={theme.status.warning}> (OAuth not authenticated)</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={serverName} flexDirection="column" marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={statusColor}>{statusIndicator} </Text>
|
||||
<Text bold>{serverDisplayName}</Text>
|
||||
<Text>
|
||||
{' - '}
|
||||
{statusText}
|
||||
{status === MCPServerStatus.CONNECTED &&
|
||||
parts.length > 0 &&
|
||||
` (${parts.join(', ')})`}
|
||||
</Text>
|
||||
{authStatusNode}
|
||||
</Box>
|
||||
{status === MCPServerStatus.CONNECTING && (
|
||||
<Text> (tools and prompts will appear when ready)</Text>
|
||||
)}
|
||||
{status === MCPServerStatus.DISCONNECTED && toolCount > 0 && (
|
||||
<Text> ({toolCount} tools cached)</Text>
|
||||
)}
|
||||
|
||||
{showDescriptions && server?.description && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{server.description.trim()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{serverTools.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
<Text color={theme.text.primary}>Tools:</Text>
|
||||
{serverTools.map((tool) => {
|
||||
const schemaContent =
|
||||
showSchema &&
|
||||
tool.schema &&
|
||||
(tool.schema.parametersJsonSchema || tool.schema.parameters)
|
||||
? JSON.stringify(
|
||||
tool.schema.parametersJsonSchema ??
|
||||
tool.schema.parameters,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Box key={tool.name} flexDirection="column">
|
||||
<Text>
|
||||
- <Text color={theme.text.primary}>{tool.name}</Text>
|
||||
</Text>
|
||||
{showDescriptions && tool.description && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{tool.description.trim()}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{schemaContent && (
|
||||
<Box flexDirection="column" marginLeft={4}>
|
||||
<Text color={theme.text.secondary}>Parameters:</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{schemaContent}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{serverPrompts.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
<Text color={theme.text.primary}>Prompts:</Text>
|
||||
{serverPrompts.map((prompt) => (
|
||||
<Box key={prompt.name} flexDirection="column">
|
||||
<Text>
|
||||
- <Text color={theme.text.primary}>{prompt.name}</Text>
|
||||
</Text>
|
||||
{showDescriptions && prompt.description && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.text.primary}>
|
||||
{prompt.description.trim()}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{blockedServers.map((server) => (
|
||||
<Box key={server.name} marginBottom={1}>
|
||||
<Text color={theme.status.error}>🔴 </Text>
|
||||
<Text bold>
|
||||
{server.name}
|
||||
{server.extensionName ? ` (from ${server.extensionName})` : ''}
|
||||
</Text>
|
||||
<Text> - Blocked</Text>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{showTips && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.accent}>💡 Tips:</Text>
|
||||
<Text>
|
||||
{' '}- Use <Text color={theme.text.accent}>/mcp desc</Text> to show
|
||||
server and tool descriptions
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Use <Text color={theme.text.accent}>/mcp schema</Text> to
|
||||
show tool parameter schemas
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Use <Text color={theme.text.accent}>/mcp nodesc</Text> to
|
||||
hide descriptions
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Use{' '}
|
||||
<Text color={theme.text.accent}>/mcp auth <server-name></Text>{' '}
|
||||
to authenticate with OAuth-enabled servers
|
||||
</Text>
|
||||
<Text>
|
||||
{' '}- Press <Text color={theme.text.accent}>Ctrl+T</Text> to
|
||||
toggle tool descriptions on/off
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
62
packages/cli/src/ui/components/views/ToolsList.test.tsx
Normal file
62
packages/cli/src/ui/components/views/ToolsList.test.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ToolsList } from './ToolsList.js';
|
||||
import { type ToolDefinition } from '../../types.js';
|
||||
|
||||
const mockTools: ToolDefinition[] = [
|
||||
{
|
||||
name: 'test-tool-one',
|
||||
displayName: 'Test Tool One',
|
||||
description: 'This is the first test tool.',
|
||||
},
|
||||
{
|
||||
name: 'test-tool-two',
|
||||
displayName: 'Test Tool Two',
|
||||
description: `This is the second test tool.
|
||||
1. Tool descriptions support markdown formatting.
|
||||
2. **note** use this tool wisely and be sure to consider how this tool interacts with word wrap.
|
||||
3. **important** this tool is awesome.`,
|
||||
},
|
||||
{
|
||||
name: 'test-tool-three',
|
||||
displayName: 'Test Tool Three',
|
||||
description: 'This is the third test tool.',
|
||||
},
|
||||
];
|
||||
|
||||
describe('<ToolsList />', () => {
|
||||
it('renders correctly with descriptions', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolsList
|
||||
tools={mockTools}
|
||||
showDescriptions={true}
|
||||
terminalWidth={40}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly without descriptions', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolsList
|
||||
tools={mockTools}
|
||||
showDescriptions={false}
|
||||
terminalWidth={40}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with no tools', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolsList tools={[]} showDescriptions={true} terminalWidth={40} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
52
packages/cli/src/ui/components/views/ToolsList.tsx
Normal file
52
packages/cli/src/ui/components/views/ToolsList.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type ToolDefinition } from '../../types.js';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
|
||||
interface ToolsListProps {
|
||||
tools: readonly ToolDefinition[];
|
||||
showDescriptions: boolean;
|
||||
terminalWidth: number;
|
||||
}
|
||||
|
||||
export const ToolsList: React.FC<ToolsListProps> = ({
|
||||
tools,
|
||||
showDescriptions,
|
||||
terminalWidth,
|
||||
}) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Available Gemini CLI tools:
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{tools.length > 0 ? (
|
||||
tools.map((tool) => (
|
||||
<Box key={tool.name} flexDirection="row">
|
||||
<Text color={theme.text.primary}>{' '}- </Text>
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.text.accent}>
|
||||
{tool.displayName}
|
||||
{showDescriptions ? ` (${tool.name})` : ''}
|
||||
</Text>
|
||||
{showDescriptions && tool.description && (
|
||||
<MarkdownDisplay
|
||||
terminalWidth={terminalWidth}
|
||||
text={tool.description}
|
||||
isPending={false}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Text color={theme.text.primary}> No tools available</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1,166 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`McpStatus > renders correctly when discovery is in progress 1`] = `
|
||||
"⏳ MCP servers are starting up (0 initializing)...
|
||||
Note: First startup may take longer. Tool availability will update automatically.
|
||||
|
||||
Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with a blocked server 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
|
||||
🔴 server-1 (from test-extension) - Blocked
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with a connected server 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with a connecting server 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with a disconnected server 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with authenticated OAuth status 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool) (OAuth)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with expired OAuth status 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool) (OAuth expired)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with parametersJsonSchema 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
Parameters:
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param1": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with prompts 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool, 1 prompt)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
Prompts:
|
||||
- prompt-1
|
||||
A test prompt
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with schema enabled 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
Parameters:
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param1": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with tips enabled 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
|
||||
|
||||
💡 Tips:
|
||||
- Use /mcp desc to show server and tool descriptions
|
||||
- Use /mcp schema to show tool parameter schemas
|
||||
- Use /mcp nodesc to hide descriptions
|
||||
- Use /mcp auth <server-name> to authenticate with OAuth-enabled servers
|
||||
- Press Ctrl+T to toggle tool descriptions on/off"
|
||||
`;
|
||||
|
||||
exports[`McpStatus > renders correctly with unauthenticated OAuth status 1`] = `
|
||||
"Configured MCP servers:
|
||||
|
||||
🟢 server-1 - Ready (1 tool) (OAuth not authenticated)
|
||||
A test server
|
||||
Tools:
|
||||
- tool-1
|
||||
A test tool
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,32 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ToolsList /> > renders correctly with descriptions 1`] = `
|
||||
"Available Gemini CLI tools:
|
||||
|
||||
- Test Tool One (test-tool-one)
|
||||
This is the first test tool.
|
||||
- Test Tool Two (test-tool-two)
|
||||
This is the second test tool.
|
||||
1. Tool descriptions support markdown formatting.
|
||||
2. note use this tool wisely and be sure to consider how this tool interacts with word wrap.
|
||||
3. important this tool is awesome.
|
||||
- Test Tool Three (test-tool-three)
|
||||
This is the third test tool.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolsList /> > renders correctly with no tools 1`] = `
|
||||
"Available Gemini CLI tools:
|
||||
|
||||
No tools available
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<ToolsList /> > renders correctly without descriptions 1`] = `
|
||||
"Available Gemini CLI tools:
|
||||
|
||||
- Test Tool One
|
||||
- Test Tool Two
|
||||
- Test Tool Three
|
||||
"
|
||||
`;
|
||||
Reference in New Issue
Block a user