diff --git a/ui/desktop/src/components/BottomMenuModeSelection.tsx b/ui/desktop/src/components/BottomMenuModeSelection.tsx index 7e3557ac..6c7fb04d 100644 --- a/ui/desktop/src/components/BottomMenuModeSelection.tsx +++ b/ui/desktop/src/components/BottomMenuModeSelection.tsx @@ -6,29 +6,36 @@ import { filterGooseModes, ModeSelectionItem, } from './settings/basic/ModeSelectionItem'; +import { useConfig } from './ConfigContext'; export const BottomMenuModeSelection = () => { const [isGooseModeMenuOpen, setIsGooseModeMenuOpen] = useState(false); const [gooseMode, setGooseMode] = useState('auto'); const [previousApproveModel, setPreviousApproveModel] = useState(''); const gooseModeDropdownRef = useRef(null); + const { read, upsert } = useConfig(); useEffect(() => { const fetchCurrentMode = async () => { try { - const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - }); + if (!process.env.ALPHA) { + const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + }); - if (response.ok) { - const { value } = await response.json(); - if (value) { - setGooseMode(value); + if (response.ok) { + const { value } = await response.json(); + if (value) { + setGooseMode(value); + } } + } else { + const mode = (await read('GOOSE_MODE', false)) as string; + setGooseMode(mode); } } catch (error) { console.error('Error fetching current mode:', error); @@ -77,28 +84,37 @@ export const BottomMenuModeSelection = () => { if (gooseMode === newMode) { return; } - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: 'GOOSE_MODE', - value: newMode, - isSecret: false, - }), - }); - if (!storeResponse.ok) { - const errorText = await storeResponse.text(); - console.error('Store response error:', errorText); - throw new Error(`Failed to store new goose mode: ${newMode}`); + if (!process.env.ALPHA) { + const storeResponse = await fetch(getApiUrl('/configs/store'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify({ + key: 'GOOSE_MODE', + value: newMode, + isSecret: false, + }), + }); + + if (!storeResponse.ok) { + const errorText = await storeResponse.text(); + console.error('Store response error:', errorText); + throw new Error(`Failed to store new goose mode: ${newMode}`); + } + if (gooseMode.includes('approve')) { + setPreviousApproveModel(gooseMode); + } + setGooseMode(newMode); + } else { + await upsert('GOOSE_MODE', newMode, false); + if (gooseMode.includes('approve')) { + setPreviousApproveModel(gooseMode); + } + setGooseMode(newMode); } - if (gooseMode.includes('approve')) { - setPreviousApproveModel(gooseMode); - } - setGooseMode(newMode); }; function getValueByKey(key) { diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx index 3850e4e2..9ccbec72 100644 --- a/ui/desktop/src/components/settings_v2/SettingsView.tsx +++ b/ui/desktop/src/components/settings_v2/SettingsView.tsx @@ -4,6 +4,7 @@ import BackButton from '../ui/BackButton'; import type { View } from '../../App'; import ExtensionsSection from './extensions/ExtensionsSection'; import ModelsSection from './models/ModelsSection'; +import { ModeSection } from './mode/ModeSection'; export type SettingsViewOptions = { extensionId?: string; @@ -36,6 +37,8 @@ export default function SettingsView({ {/* Extensions Section */} + {/* Goose Modes */} + diff --git a/ui/desktop/src/components/settings_v2/mode/ConfigureApproveMode.tsx b/ui/desktop/src/components/settings_v2/mode/ConfigureApproveMode.tsx new file mode 100644 index 00000000..621ee3f5 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/mode/ConfigureApproveMode.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react'; +import { Card } from '../../ui/card'; +import { Button } from '../../ui/button'; +import { GooseMode, ModeSelectionItem } from './ModeSelectionItem'; + +interface ConfigureApproveModeProps { + onClose: () => void; + handleModeChange: (newMode: string) => void; + currentMode: string | null; +} + +export function ConfigureApproveMode({ + onClose, + handleModeChange, + currentMode, +}: ConfigureApproveModeProps) { + const approveModes: GooseMode[] = [ + { + key: 'approve', + label: 'Manual Approval', + description: 'All tools, extensions and file modifications will require human approval', + }, + { + key: 'smart_approve', + label: 'Smart Approval', + description: 'Intelligently determine which actions need approval based on risk level ', + }, + ]; + + const [isSubmitting, setIsSubmitting] = useState(false); + const [approveMode, setApproveMode] = useState(currentMode); + + useEffect(() => { + setApproveMode(currentMode); + }, [currentMode]); + + const handleModeSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + setIsSubmitting(true); + try { + handleModeChange(approveMode); + onClose(); + } catch (error) { + console.error('Error configuring goose mode:', error); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ +
+ {/* Header */} +
+

+ Configure Approve Mode +

+
+ +
+

+ Approve requests can either be given to all tool requests or determine which actions + may need integration +

+
+ {approveModes.map((mode) => ( + { + setApproveMode(newMode); + }} + /> + ))} +
+
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx new file mode 100644 index 00000000..bacde850 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from 'react'; +import { getApiUrl, getSecretKey } from '../../../config'; +import { all_goose_modes, filterGooseModes, ModeSelectionItem } from './ModeSelectionItem'; +import ExtensionList from '@/src/components/settings_v2/extensions/subcomponents/ExtensionList'; +import { Button } from '@/src/components/ui/button'; +import { Plus } from 'lucide-react'; +import { GPSIcon } from '@/src/components/ui/icons'; + +export const ModeSection = () => { + const [currentMode, setCurrentMode] = useState('auto'); + const [previousApproveModel, setPreviousApproveModel] = useState(''); + + const handleModeChange = async (newMode: string) => { + const storeResponse = await fetch(getApiUrl('/configs/store'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify({ + key: 'GOOSE_MODE', + value: newMode, + isSecret: false, + }), + }); + + if (!storeResponse.ok) { + const errorText = await storeResponse.text(); + console.error('Store response error:', errorText); + throw new Error(`Failed to store new goose mode: ${newMode}`); + } + // Only track the previous approve if current mode is approve related but new mode is not. + if (currentMode.includes('approve') && !newMode.includes('approve')) { + setPreviousApproveModel(currentMode); + } + setCurrentMode(newMode); + }; + + useEffect(() => { + const fetchCurrentMode = async () => { + try { + const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + }); + + if (response.ok) { + const { value } = await response.json(); + if (value) { + setCurrentMode(value); + } + } + } catch (error) { + console.error('Error fetching current mode:', error); + } + }; + + fetchCurrentMode(); + }, []); + + return ( +
+
+

Mode

+
+
+

+ Configure how Goose interacts with tools and extensions +

+
+ {filterGooseModes(currentMode, all_goose_modes, previousApproveModel).map((mode) => ( + + ))} +
+
+
+ ); +}; diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx b/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx new file mode 100644 index 00000000..f9de90ce --- /dev/null +++ b/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx @@ -0,0 +1,146 @@ +import React, { useEffect, useState } from 'react'; +import { Gear } from '../../icons'; +import { ConfigureApproveMode } from './ConfigureApproveMode'; + +export interface GooseMode { + key: string; + label: string; + description: string; +} + +export const all_goose_modes: GooseMode[] = [ + { + key: 'auto', + label: 'Completely Autonomous', + description: 'Full file modification capabilities, edit, create, and delete files freely.', + }, + { + key: 'approve', + label: 'Manual Approval', + description: 'All tools, extensions and file modifications will require human approval', + }, + { + key: 'smart_approve', + label: 'Smart Approval', + description: 'Intelligently determine which actions need approval based on risk level ', + }, + { + key: 'chat', + label: 'Chat Only', + description: 'Engage with the selected provider without using tools or extensions.', + }, +]; + +export function filterGooseModes( + currentMode: string, + modes: GooseMode[], + previousApproveMode: string +) { + return modes.filter((mode) => { + const approveList = ['approve', 'smart_approve']; + const nonApproveList = ['auto', 'chat']; + // Always keep 'auto' and 'chat' + if (nonApproveList.includes(mode.key)) { + return true; + } + // If current mode is non approve mode, we display write approve by default. + if (nonApproveList.includes(currentMode) && !previousApproveMode) { + return mode.key === 'smart_approve'; + } + + // Always include the current and previou approve mode + if (mode.key === currentMode) { + return true; + } + + // Current mode and previous approve mode cannot exist at the same time. + if (approveList.includes(currentMode) && approveList.includes(previousApproveMode)) { + return false; + } + + if (mode.key === previousApproveMode) { + return true; + } + + return false; + }); +} + +interface ModeSelectionItemProps { + currentMode: string; + mode: GooseMode; + showDescription: boolean; + isApproveModeConfigure: boolean; + handleModeChange: (newMode: string) => void; +} + +export function ModeSelectionItem({ + currentMode, + mode, + showDescription, + isApproveModeConfigure, + handleModeChange, +}: ModeSelectionItemProps) { + const [checked, setChecked] = useState(currentMode == mode.key); + const [isDislogOpen, setIsDislogOpen] = useState(false); + + useEffect(() => { + setChecked(currentMode === mode.key); + }, [currentMode, mode.key]); + + return ( +
+
handleModeChange(mode.key)} + > +
+

{mode.label}

+ {showDescription && ( +

+ {mode.description} +

+ )} +
+
+ {!isApproveModeConfigure && (mode.key == 'approve' || mode.key == 'smart_approve') && ( + + )} + handleModeChange(mode.key)} + className="peer sr-only" + /> +
+
+
+
+
+ {isDislogOpen ? ( + { + setIsDislogOpen(false); + }} + handleModeChange={handleModeChange} + currentMode={currentMode} + /> + ) : null} +
+
+
+ ); +}