mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-05 23:56:45 -07:00
feat(workspaces): implement Workspaces UI and action primitives
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
type Config,
|
||||
type WorkspaceHubInfo
|
||||
} from '@google-gemini-cli-core';
|
||||
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
|
||||
import type { SlashCommand, CommandContext } from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import type { MessageActionReturn } from '@google/gemini-cli-core';
|
||||
import { WorkspaceHubClient } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type CommandActionReturn,
|
||||
WorkspaceHubClient
|
||||
} from '@google-gemini-cli-core';
|
||||
|
||||
const listAction = async (
|
||||
_context: CommandContext,
|
||||
): Promise<void | MessageActionReturn> => {
|
||||
): Promise<CommandActionReturn> => {
|
||||
const hubUrl =
|
||||
process.env['GEMINI_WORKSPACE_HUB_URL'] || 'http://localhost:8080';
|
||||
const client = new WorkspaceHubClient(hubUrl);
|
||||
@@ -27,21 +29,12 @@ const listAction = async (
|
||||
};
|
||||
}
|
||||
|
||||
let content = 'Active Workspaces:\n';
|
||||
content += '------------------------------------------------------------\n';
|
||||
for (const ws of workspaces) {
|
||||
content += `${ws.name.padEnd(20)} | ${ws.status.padEnd(12)} | ${ws.id}\n`;
|
||||
}
|
||||
content += '------------------------------------------------------------';
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content,
|
||||
type: 'workspaces_list',
|
||||
workspaces,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const message = (error as Error).message;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
@@ -56,7 +49,7 @@ const listCommand: SlashCommand = {
|
||||
description: 'List remote workspaces',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => listAction(context),
|
||||
action: ((context: CommandContext) => listAction(context)) as any,
|
||||
};
|
||||
|
||||
const createCommand: SlashCommand = {
|
||||
@@ -64,10 +57,10 @@ const createCommand: SlashCommand = {
|
||||
description: 'Create a new remote workspace',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
action: (async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
): Promise<CommandActionReturn> => {
|
||||
const name = args.trim();
|
||||
if (!name) {
|
||||
return {
|
||||
@@ -93,15 +86,14 @@ const createCommand: SlashCommand = {
|
||||
content: `✅ Workspace created successfully!\nID: ${ws.id}\nName: ${ws.name}\nGCE: ${ws.instance_name}`,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const message = (error as Error).message;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to create workspace: ${message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
}) as any,
|
||||
};
|
||||
|
||||
const deleteCommand: SlashCommand = {
|
||||
@@ -110,10 +102,10 @@ const deleteCommand: SlashCommand = {
|
||||
description: 'Delete a remote workspace',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
action: (async (
|
||||
_context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
): Promise<CommandActionReturn> => {
|
||||
const id = args.trim();
|
||||
if (!id) {
|
||||
return {
|
||||
@@ -128,7 +120,8 @@ const deleteCommand: SlashCommand = {
|
||||
const client = new WorkspaceHubClient(hubUrl);
|
||||
|
||||
try {
|
||||
context.ui.addItem({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
(_context.ui as any).addItem({
|
||||
type: 'info',
|
||||
text: `Deleting workspace "${id}"...`,
|
||||
});
|
||||
@@ -139,15 +132,14 @@ const deleteCommand: SlashCommand = {
|
||||
content: `✅ Workspace ${id} deleted successfully.`,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const message = (error as Error).message;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to delete workspace: ${message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
}) as any,
|
||||
};
|
||||
|
||||
const connectCommand: SlashCommand = {
|
||||
@@ -155,10 +147,10 @@ const connectCommand: SlashCommand = {
|
||||
description: 'Connect to a remote workspace',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
action: (async (
|
||||
_context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
): Promise<CommandActionReturn> => {
|
||||
const id = args.trim();
|
||||
if (!id) {
|
||||
return {
|
||||
@@ -171,7 +163,7 @@ const connectCommand: SlashCommand = {
|
||||
type: 'submit_prompt',
|
||||
content: `I want to connect to remote workspace "${id}". Please run the connect command.`,
|
||||
};
|
||||
},
|
||||
}) as any,
|
||||
};
|
||||
|
||||
export const workspaceSlashCommand: SlashCommand = {
|
||||
@@ -181,5 +173,5 @@ export const workspaceSlashCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [listCommand, createCommand, deleteCommand, connectCommand],
|
||||
action: async (context: CommandContext) => listAction(context),
|
||||
action: (async (context: CommandContext) => listAction(context)) as any,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ import { SkillsList } from './views/SkillsList.js';
|
||||
import { AgentsStatus } from './views/AgentsStatus.js';
|
||||
import { McpStatus } from './views/McpStatus.js';
|
||||
import { ChatList } from './views/ChatList.js';
|
||||
import { WorkspacesList } from './views/WorkspacesList.js';
|
||||
import { ModelMessage } from './messages/ModelMessage.js';
|
||||
import { ThinkingMessage } from './messages/ThinkingMessage.js';
|
||||
import { HintMessage } from './messages/HintMessage.js';
|
||||
@@ -233,6 +234,9 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'chat_list' && (
|
||||
<ChatList chats={itemForDisplay.chats} />
|
||||
)}
|
||||
{itemForDisplay.type === 'workspaces_list' && (
|
||||
<WorkspacesList workspaces={itemForDisplay.workspaces} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { WorkspaceHubInfo } from '@google/gemini-cli-core';
|
||||
|
||||
interface WorkspacesListProps {
|
||||
workspaces: readonly WorkspaceHubInfo[];
|
||||
}
|
||||
|
||||
export const WorkspacesList: React.FC<WorkspacesListProps> = ({ workspaces }) => {
|
||||
if (workspaces.length === 0) {
|
||||
return <Text>No active workspaces found.</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold underline>Active Remote Workspaces:</Text>
|
||||
<Box flexDirection="column" paddingLeft={2} marginTop={1}>
|
||||
{workspaces.map((ws) => {
|
||||
const isReady = ws.status === 'READY';
|
||||
const statusColor = isReady ? 'green' : 'yellow';
|
||||
|
||||
return (
|
||||
<Box key={ws.id} flexDirection="column" marginBottom={1}>
|
||||
<Box>
|
||||
<Text color="cyan" bold>{ws.name.padEnd(20)}</Text>
|
||||
<Text> | </Text>
|
||||
<Text color={statusColor}>{ws.status.padEnd(12)}</Text>
|
||||
<Text> | </Text>
|
||||
<Text dimColor>{ws.id}</Text>
|
||||
</Box>
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>Instance: {ws.instance_name} ({ws.zone})</Text>
|
||||
</Box>
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>Project: {ws.project_id}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor italic>Use `/wsr connect {'<name>'}` to teleport into a workspace.</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -662,6 +662,16 @@ export const useSlashCommandProcessor = (
|
||||
setCustomDialog(result.component);
|
||||
return { type: 'handled' };
|
||||
}
|
||||
case 'workspaces_list': {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.WORKSPACES_LIST,
|
||||
workspaces: result.workspaces,
|
||||
} as any,
|
||||
Date.now(),
|
||||
);
|
||||
return { type: 'handled' };
|
||||
}
|
||||
default: {
|
||||
const unhandled: never = result;
|
||||
throw new Error(
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type AgentDefinition,
|
||||
type ApprovalMode,
|
||||
type Kind,
|
||||
type WorkspaceHubInfo,
|
||||
CoreToolCallStatus,
|
||||
checkExhaustive,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -272,6 +273,11 @@ export type HistoryItemChatList = HistoryItemBase & {
|
||||
chats: ChatDetail[];
|
||||
};
|
||||
|
||||
export type HistoryItemWorkspacesList = HistoryItemBase & {
|
||||
type: 'workspaces_list';
|
||||
workspaces: WorkspaceHubInfo[];
|
||||
};
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
displayName: string;
|
||||
@@ -379,6 +385,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemAgentsList
|
||||
| HistoryItemMcpStatus
|
||||
| HistoryItemChatList
|
||||
| HistoryItemWorkspacesList
|
||||
| HistoryItemThinking
|
||||
| HistoryItemHint;
|
||||
|
||||
@@ -404,6 +411,7 @@ export enum MessageType {
|
||||
AGENTS_LIST = 'agents_list',
|
||||
MCP_STATUS = 'mcp_status',
|
||||
CHAT_LIST = 'chat_list',
|
||||
WORKSPACES_LIST = 'workspaces_list',
|
||||
HINT = 'hint',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user