summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
blob: abc4fe61eb63d3fa31f223c8362d90a8efc80c2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import React from 'react';
import { Box, Text, useInput } from 'ink';
import SelectInput from 'ink-select-input';
import { PartListUnion } from '@google/genai';
import { DiffRenderer } from './DiffRenderer.js';
import { UI_WIDTH } from '../../constants.js';
import { Colors } from '../../colors.js';
import {
  ToolCallConfirmationDetails,
  ToolEditConfirmationDetails,
  ToolConfirmationOutcome,
  ToolExecuteConfirmationDetails,
} from '@gemini-code/server';

export interface ToolConfirmationMessageProps {
  confirmationDetails: ToolCallConfirmationDetails;
  onSubmit: (value: PartListUnion) => void;
}

function isEditDetails(
  props: ToolCallConfirmationDetails,
): props is ToolEditConfirmationDetails {
  return (props as ToolEditConfirmationDetails).fileName !== undefined;
}

interface InternalOption {
  label: string;
  value: ToolConfirmationOutcome;
}

export const ToolConfirmationMessage: React.FC<
  ToolConfirmationMessageProps
> = ({ confirmationDetails }) => {
  const { onConfirm } = confirmationDetails;

  useInput((_, key) => {
    if (key.escape) {
      onConfirm(ToolConfirmationOutcome.Cancel);
    }
  });

  const handleSelect = (item: InternalOption) => {
    onConfirm(item.value);
  };

  let bodyContent: React.ReactNode | null = null; // Removed contextDisplay here
  let question: string;
  const options: InternalOption[] = [];

  if (isEditDetails(confirmationDetails)) {
    // Body content is now the DiffRenderer, passing filename to it
    // The bordered box is removed from here and handled within DiffRenderer
    bodyContent = <DiffRenderer diffContent={confirmationDetails.fileDiff} />;

    question = `Apply this change?`;
    options.push(
      {
        label: 'Yes',
        value: ToolConfirmationOutcome.ProceedOnce,
      },
      {
        label: 'Yes (always allow)',
        value: ToolConfirmationOutcome.ProceedAlways,
      },
      { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
    );
  } else {
    const executionProps =
      confirmationDetails as ToolExecuteConfirmationDetails;

    // For execution, we still need context display and description
    const commandDisplay = (
      <Text color={Colors.AccentCyan}>{executionProps.command}</Text>
    );

    // Combine command and description into bodyContent for layout consistency
    bodyContent = (
      <Box flexDirection="column">
        <Box paddingX={1} marginLeft={1}>
          {commandDisplay}
        </Box>
      </Box>
    );

    question = `Allow execution?`;
    const alwaysLabel = `Yes (always allow '${executionProps.rootCommand}' commands)`;
    options.push(
      {
        label: 'Yes',
        value: ToolConfirmationOutcome.ProceedOnce,
      },
      {
        label: alwaysLabel,
        value: ToolConfirmationOutcome.ProceedAlways,
      },
      { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
    );
  }

  return (
    <Box flexDirection="column" padding={1} minWidth={UI_WIDTH}>
      {/* Body Content (Diff Renderer or Command Info) */}
      {/* No separate context display here anymore for edits */}
      <Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
        {bodyContent}
      </Box>

      {/* Confirmation Question */}
      <Box marginBottom={1} flexShrink={0}>
        <Text>{question}</Text>
      </Box>

      {/* Select Input for Options */}
      <Box flexShrink={0}>
        <SelectInput
          indicatorComponent={Indicator}
          itemComponent={Item}
          items={options}
          onSelect={handleSelect}
        />
      </Box>
    </Box>
  );
};

function Indicator({ isSelected = false }): React.JSX.Element {
  return (
    <Text color={isSelected ? Colors.AccentGreen : Colors.Gray}>
      {isSelected ? '◉ ' : '○ '}
    </Text>
  );
}

export type ItemProps = {
  readonly isSelected?: boolean;
  readonly label: string;
};

function Item({ isSelected = false, label }: ItemProps): React.JSX.Element {
  return (
    <Text color={isSelected ? Colors.AccentGreen : Colors.Gray}>{label}</Text>
  );
}