feat: init UI implementation for tool permission control (#2194)

This commit is contained in:
Yingjie He
2025-04-15 16:49:49 -07:00
committed by GitHub
parent 0ddf8b1459
commit 92ce1be317
6 changed files with 142 additions and 12 deletions
+4 -1
View File
@@ -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' && <PermissionSettingsView onClose={() => setView('settings')} />}
</div>
</div>
{isGoosehintsModalOpen && (
@@ -43,7 +43,7 @@ export default function SettingsView({
showEnvVars={viewOptions.showEnvVars}
/>
{/* Goose Modes */}
<ModeSection />
<ModeSection setView={setView} />
{/*Session sharing*/}
<SessionSharingSection />
</div>
@@ -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
</p>
<div>
{filterGooseModes(currentMode, all_goose_modes, previousApproveModel).map((mode) => (
{all_goose_modes.map((mode) => (
<ModeSelectionItem
key={mode.key}
mode={mode}
currentMode={currentMode}
showDescription={true}
isApproveModeConfigure={false}
setView={setView}
handleModeChange={handleModeChange}
/>
))}
@@ -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') && (
<button
onClick={() => {
setIsDislogOpen(true);
setView('permission');
}}
>
<Gear className="w-5 h-5 text-textSubtle hover:text-textStandard" />
@@ -149,7 +149,7 @@ export default function PermissionModal({ extensionName, onClose }: PermissionMo
<label className="block text-sm font-medium text-textStandard">
{tool.name}
</label>
<p className="text-sm text-gray-500 mb-2">
<p className="text-sm text-textSubtle mb-2">
{getFirstSentence(tool.description)}
</p>
</div>
@@ -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 (
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-textStandard">{title}</h3>
<p className="text-xs text-textSubtle mt-1">{description}</p>
</div>
<div>
<button onClick={() => setIsModalOpen(true)}>
<ChevronRight className="w-4 h-4 text-iconStandard" />
</button>
</div>
{/* Modal for updating tool permission */}
{isModalOpen && <PermissionModal onClose={handleModalClose} extensionName={title} />}
</div>
);
}
function RulesSection({ title, rules }: { title: string; rules: React.ReactNode }) {
return (
<div className="space-y-4">
<h2 className="text-xl font-medium text-textStandard">{title}</h2>
{rules}
</div>
);
}
export default function PermissionSettingsView({ onClose }: { onClose: () => void }) {
const { getExtensions } = useConfig();
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
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 (
<div className="h-screen w-full animate-[fadein_200ms_ease-in_forwards]">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
<ScrollArea className="h-full w-full">
<div className="flex flex-col pb-24">
<div className="px-8 pt-6 pb-4">
<BackButton onClick={() => onClose()} className="mb-4" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
>
<circle cx="32" cy="32" r="32" fill="#101010" />
<rect x="22" y="17" width="8" height="8" rx="4" fill="white" />
<rect x="22" y="28" width="20" height="20" rx="10" fill="white" />
</svg>
<h1 className="text-3xl font-medium text-textStandard mt-4">Permission Rules</h1>
<p className="text-textSubtle">
Hidden instructions that will be passed to the provider to help direct and add context
to your responses.
</p>
</div>
{/* Content Area */}
<div className="flex-1 pt-[20px]">
<div className="space-y-8 px-8">
{/* Extension Rules Section */}
<RulesSection
title="Extension rules"
rules={
<>
{extensions.map((extension) => (
<RuleItem
key={extension.name}
title={extension.name}
description={'description' in extension ? extension.description || '' : ''}
/>
))}
</>
}
/>
</div>
</div>
</div>
</ScrollArea>
</div>
);
}