feat(workspaces): implement Workspaces UI and action primitives

This commit is contained in:
mkorwel
2026-03-19 09:43:39 -07:00
parent 26ce07d89b
commit 309bae18da
12 changed files with 154 additions and 52 deletions
@@ -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(
+8
View File
@@ -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',
}