mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat: init UI implementation for tool permission control (#2194)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user