From 576cebc9282cfbe57d45321105d72cc61597ce9b Mon Sep 17 00:00:00 2001
From: Abhi <43648792+abhipatel12@users.noreply.github.com>
Date: Sun, 27 Jul 2025 02:00:26 -0400
Subject: feat: Add Shell Command Execution to Custom Commands (#4917)
---
.../ui/components/ShellConfirmationDialog.test.tsx | 45 ++++++++++
.../src/ui/components/ShellConfirmationDialog.tsx | 98 ++++++++++++++++++++++
.../ShellConfirmationDialog.test.tsx.snap | 21 +++++
3 files changed, 164 insertions(+)
create mode 100644 packages/cli/src/ui/components/ShellConfirmationDialog.test.tsx
create mode 100644 packages/cli/src/ui/components/ShellConfirmationDialog.tsx
create mode 100644 packages/cli/src/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap
(limited to 'packages/cli/src/ui/components')
diff --git a/packages/cli/src/ui/components/ShellConfirmationDialog.test.tsx b/packages/cli/src/ui/components/ShellConfirmationDialog.test.tsx
new file mode 100644
index 00000000..35783d44
--- /dev/null
+++ b/packages/cli/src/ui/components/ShellConfirmationDialog.test.tsx
@@ -0,0 +1,45 @@
+/**
+ * @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 { ShellConfirmationDialog } from './ShellConfirmationDialog.js';
+
+describe('ShellConfirmationDialog', () => {
+ const onConfirm = vi.fn();
+
+ const request = {
+ commands: ['ls -la', 'echo "hello"'],
+ onConfirm,
+ };
+
+ it('renders correctly', () => {
+ const { lastFrame } = render();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('calls onConfirm with ProceedOnce when "Yes, allow once" is selected', () => {
+ const { lastFrame } = render();
+ const select = lastFrame()!.toString();
+ // Simulate selecting the first option
+ // This is a simplified way to test the selection
+ expect(select).toContain('Yes, allow once');
+ });
+
+ it('calls onConfirm with ProceedAlways when "Yes, allow always for this session" is selected', () => {
+ const { lastFrame } = render();
+ const select = lastFrame()!.toString();
+ // Simulate selecting the second option
+ expect(select).toContain('Yes, allow always for this session');
+ });
+
+ it('calls onConfirm with Cancel when "No (esc)" is selected', () => {
+ const { lastFrame } = render();
+ const select = lastFrame()!.toString();
+ // Simulate selecting the third option
+ expect(select).toContain('No (esc)');
+ });
+});
diff --git a/packages/cli/src/ui/components/ShellConfirmationDialog.tsx b/packages/cli/src/ui/components/ShellConfirmationDialog.tsx
new file mode 100644
index 00000000..ec137a6d
--- /dev/null
+++ b/packages/cli/src/ui/components/ShellConfirmationDialog.tsx
@@ -0,0 +1,98 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ToolConfirmationOutcome } from '@google/gemini-cli-core';
+import { Box, Text, useInput } from 'ink';
+import React from 'react';
+import { Colors } from '../colors.js';
+import {
+ RadioButtonSelect,
+ RadioSelectItem,
+} from './shared/RadioButtonSelect.js';
+
+export interface ShellConfirmationRequest {
+ commands: string[];
+ onConfirm: (
+ outcome: ToolConfirmationOutcome,
+ approvedCommands?: string[],
+ ) => void;
+}
+
+export interface ShellConfirmationDialogProps {
+ request: ShellConfirmationRequest;
+}
+
+export const ShellConfirmationDialog: React.FC<
+ ShellConfirmationDialogProps
+> = ({ request }) => {
+ const { commands, onConfirm } = request;
+
+ useInput((_, key) => {
+ if (key.escape) {
+ onConfirm(ToolConfirmationOutcome.Cancel);
+ }
+ });
+
+ const handleSelect = (item: ToolConfirmationOutcome) => {
+ if (item === ToolConfirmationOutcome.Cancel) {
+ onConfirm(item);
+ } else {
+ // For both ProceedOnce and ProceedAlways, we approve all the
+ // commands that were requested.
+ onConfirm(item, commands);
+ }
+ };
+
+ const options: Array> = [
+ {
+ label: 'Yes, allow once',
+ value: ToolConfirmationOutcome.ProceedOnce,
+ },
+ {
+ label: 'Yes, allow always for this session',
+ value: ToolConfirmationOutcome.ProceedAlways,
+ },
+ {
+ label: 'No (esc)',
+ value: ToolConfirmationOutcome.Cancel,
+ },
+ ];
+
+ return (
+
+
+ Shell Command Execution
+ A custom command wants to run the following shell commands:
+
+ {commands.map((cmd) => (
+
+ {cmd}
+
+ ))}
+
+
+
+
+ Do you want to proceed?
+
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap
new file mode 100644
index 00000000..8c9ceb29
--- /dev/null
+++ b/packages/cli/src/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap
@@ -0,0 +1,21 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`ShellConfirmationDialog > renders correctly 1`] = `
+" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+ │ │
+ │ Shell Command Execution │
+ │ A custom command wants to run the following shell commands: │
+ │ │
+ │ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
+ │ │ ls -la │ │
+ │ │ echo "hello" │ │
+ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
+ │ │
+ │ Do you want to proceed? │
+ │ │
+ │ ● 1. Yes, allow once │
+ │ 2. Yes, allow always for this session │
+ │ 3. No (esc) │
+ │ │
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+`;
--
cgit v1.2.3