From 92ce1be3176117a7267179b8ca15ba3b4f0e596e Mon Sep 17 00:00:00 2001 From: Yingjie He Date: Tue, 15 Apr 2025 16:49:49 -0700 Subject: [PATCH] feat: init UI implementation for tool permission control (#2194) --- ui/desktop/src/App.tsx | 5 +- .../components/settings_v2/SettingsView.tsx | 2 +- .../settings_v2/mode/ModeSection.tsx | 17 +-- .../settings_v2/mode/ModeSelectionItem.tsx | 5 +- .../permission/PermissionModal.tsx | 2 +- .../permission/PermissionSetting.tsx | 123 ++++++++++++++++++ 6 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/permission/PermissionSetting.tsx diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 0d73ed01..97f77158 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -35,6 +35,7 @@ import 'react-toastify/dist/ReactToastify.css'; import { useConfig, MalformedConfigError } from './components/ConfigContext'; import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions'; import { initConfig } from './api/sdk.gen'; +import PermissionSettingsView from './components/settings_v2/permission/PermissionSetting'; // Views and their options export type View = @@ -49,7 +50,8 @@ export type View = | 'sessions' | 'sharedSession' | 'loading' - | 'recipeEditor'; + | 'recipeEditor' + | 'permission'; export type ViewOptions = | SettingsViewOptions @@ -779,6 +781,7 @@ export default function App() { }} /> )} + {view === 'permission' && setView('settings')} />} {isGoosehintsModalOpen && ( diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx index 62ba4f6e..b612ed88 100644 --- a/ui/desktop/src/components/settings_v2/SettingsView.tsx +++ b/ui/desktop/src/components/settings_v2/SettingsView.tsx @@ -43,7 +43,7 @@ export default function SettingsView({ showEnvVars={viewOptions.showEnvVars} /> {/* Goose Modes */} - + {/*Session sharing*/} diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx index 7d89bdff..3f66a09f 100644 --- a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx +++ b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx @@ -1,10 +1,14 @@ import React, { useEffect, useState } from 'react'; import { getApiUrl, getSecretKey } from '../../../config'; -import { all_goose_modes, filterGooseModes, ModeSelectionItem } from './ModeSelectionItem'; +import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem'; +import { View } from '../../../App'; -export const ModeSection = () => { +interface ModeSectionProps { + setView: (view: View) => void; +} + +export const ModeSection = ({ setView }: ModeSectionProps) => { const [currentMode, setCurrentMode] = useState('auto'); - const [previousApproveModel, setPreviousApproveModel] = useState(''); const handleModeChange = async (newMode: string) => { const storeResponse = await fetch(getApiUrl('/configs/store'), { @@ -25,10 +29,6 @@ export const ModeSection = () => { 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); }; @@ -67,13 +67,14 @@ export const ModeSection = () => { Configure how Goose interacts with tools and extensions

- {filterGooseModes(currentMode, all_goose_modes, previousApproveModel).map((mode) => ( + {all_goose_modes.map((mode) => ( ))} diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx b/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx index 27a1d1c0..17cc6f98 100644 --- a/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx +++ b/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Gear } from '../../icons'; import { ConfigureApproveMode } from './ConfigureApproveMode'; +import { View } from '../../../App'; export interface GooseMode { key: string; @@ -71,6 +72,7 @@ interface ModeSelectionItemProps { mode: GooseMode; showDescription: boolean; isApproveModeConfigure: boolean; + setView: (view: View) => void; handleModeChange: (newMode: string) => void; } @@ -79,6 +81,7 @@ export function ModeSelectionItem({ mode, showDescription, isApproveModeConfigure, + setView, handleModeChange, }: ModeSelectionItemProps) { const [checked, setChecked] = useState(currentMode == mode.key); @@ -109,7 +112,7 @@ export function ModeSelectionItem({ {!isApproveModeConfigure && (mode.key == 'approve' || mode.key == 'smart_approve') && (
diff --git a/ui/desktop/src/components/settings_v2/permission/PermissionSetting.tsx b/ui/desktop/src/components/settings_v2/permission/PermissionSetting.tsx new file mode 100644 index 00000000..5d1ef8c0 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/permission/PermissionSetting.tsx @@ -0,0 +1,123 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { ScrollArea } from '../../ui/scroll-area'; +import BackButton from '../../ui/BackButton'; +import { FixedExtensionEntry, useConfig } from '../../ConfigContext'; +import { ChevronRight } from 'lucide-react'; +import PermissionModal from './PermissionModal'; + +function RuleItem({ title, description }: { title: string; description: string }) { + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalClose = () => { + console.log('close modal'); + setIsModalOpen(false); + }; + + return ( +
+
+

{title}

+

{description}

+
+
+ +
+ {/* Modal for updating tool permission */} + {isModalOpen && } +
+ ); +} + +function RulesSection({ title, rules }: { title: string; rules: React.ReactNode }) { + return ( +
+

{title}

+ {rules} +
+ ); +} + +export default function PermissionSettingsView({ onClose }: { onClose: () => void }) { + const { getExtensions } = useConfig(); + const [extensions, setExtensions] = useState([]); + + const fetchExtensions = useCallback(async () => { + const extensionsList = await getExtensions(true); // Force refresh + // Filter out disabled extensions + const enabledExtensions = extensionsList.filter((extension) => extension.enabled); + // Sort extensions by name to maintain consistent order + const sortedExtensions = [...enabledExtensions].sort((a, b) => { + // First sort by builtin + if (a.type === 'builtin' && b.type !== 'builtin') return -1; + if (a.type !== 'builtin' && b.type === 'builtin') return 1; + + // Then sort by bundled (handle null/undefined cases) + const aBundled = a.bundled === true; + const bBundled = b.bundled === true; + if (aBundled && !bBundled) return -1; + if (!aBundled && bBundled) return 1; + + // Finally sort alphabetically within each group + return a.name.localeCompare(b.name); + }); + setExtensions(sortedExtensions); + }, [getExtensions]); + + useEffect(() => { + fetchExtensions(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+ + +
+
+ onClose()} className="mb-4" /> + + + + + +

Permission Rules

+

+ Hidden instructions that will be passed to the provider to help direct and add context + to your responses. +

+
+ + {/* Content Area */} +
+
+ {/* Extension Rules Section */} + + {extensions.map((extension) => ( + + ))} + + } + /> +
+
+
+
+
+ ); +}