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 { useConfig, MalformedConfigError } from './components/ConfigContext';
|
||||||
import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions';
|
import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions';
|
||||||
import { initConfig } from './api/sdk.gen';
|
import { initConfig } from './api/sdk.gen';
|
||||||
|
import PermissionSettingsView from './components/settings_v2/permission/PermissionSetting';
|
||||||
|
|
||||||
// Views and their options
|
// Views and their options
|
||||||
export type View =
|
export type View =
|
||||||
@@ -49,7 +50,8 @@ export type View =
|
|||||||
| 'sessions'
|
| 'sessions'
|
||||||
| 'sharedSession'
|
| 'sharedSession'
|
||||||
| 'loading'
|
| 'loading'
|
||||||
| 'recipeEditor';
|
| 'recipeEditor'
|
||||||
|
| 'permission';
|
||||||
|
|
||||||
export type ViewOptions =
|
export type ViewOptions =
|
||||||
| SettingsViewOptions
|
| SettingsViewOptions
|
||||||
@@ -779,6 +781,7 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{view === 'permission' && <PermissionSettingsView onClose={() => setView('settings')} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isGoosehintsModalOpen && (
|
{isGoosehintsModalOpen && (
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function SettingsView({
|
|||||||
showEnvVars={viewOptions.showEnvVars}
|
showEnvVars={viewOptions.showEnvVars}
|
||||||
/>
|
/>
|
||||||
{/* Goose Modes */}
|
{/* Goose Modes */}
|
||||||
<ModeSection />
|
<ModeSection setView={setView} />
|
||||||
{/*Session sharing*/}
|
{/*Session sharing*/}
|
||||||
<SessionSharingSection />
|
<SessionSharingSection />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { getApiUrl, getSecretKey } from '../../../config';
|
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 [currentMode, setCurrentMode] = useState('auto');
|
||||||
const [previousApproveModel, setPreviousApproveModel] = useState('');
|
|
||||||
|
|
||||||
const handleModeChange = async (newMode: string) => {
|
const handleModeChange = async (newMode: string) => {
|
||||||
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
const storeResponse = await fetch(getApiUrl('/configs/store'), {
|
||||||
@@ -25,10 +29,6 @@ export const ModeSection = () => {
|
|||||||
console.error('Store response error:', errorText);
|
console.error('Store response error:', errorText);
|
||||||
throw new Error(`Failed to store new goose mode: ${newMode}`);
|
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);
|
setCurrentMode(newMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,13 +67,14 @@ export const ModeSection = () => {
|
|||||||
Configure how Goose interacts with tools and extensions
|
Configure how Goose interacts with tools and extensions
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
{filterGooseModes(currentMode, all_goose_modes, previousApproveModel).map((mode) => (
|
{all_goose_modes.map((mode) => (
|
||||||
<ModeSelectionItem
|
<ModeSelectionItem
|
||||||
key={mode.key}
|
key={mode.key}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
currentMode={currentMode}
|
currentMode={currentMode}
|
||||||
showDescription={true}
|
showDescription={true}
|
||||||
isApproveModeConfigure={false}
|
isApproveModeConfigure={false}
|
||||||
|
setView={setView}
|
||||||
handleModeChange={handleModeChange}
|
handleModeChange={handleModeChange}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Gear } from '../../icons';
|
import { Gear } from '../../icons';
|
||||||
import { ConfigureApproveMode } from './ConfigureApproveMode';
|
import { ConfigureApproveMode } from './ConfigureApproveMode';
|
||||||
|
import { View } from '../../../App';
|
||||||
|
|
||||||
export interface GooseMode {
|
export interface GooseMode {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -71,6 +72,7 @@ interface ModeSelectionItemProps {
|
|||||||
mode: GooseMode;
|
mode: GooseMode;
|
||||||
showDescription: boolean;
|
showDescription: boolean;
|
||||||
isApproveModeConfigure: boolean;
|
isApproveModeConfigure: boolean;
|
||||||
|
setView: (view: View) => void;
|
||||||
handleModeChange: (newMode: string) => void;
|
handleModeChange: (newMode: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +81,7 @@ export function ModeSelectionItem({
|
|||||||
mode,
|
mode,
|
||||||
showDescription,
|
showDescription,
|
||||||
isApproveModeConfigure,
|
isApproveModeConfigure,
|
||||||
|
setView,
|
||||||
handleModeChange,
|
handleModeChange,
|
||||||
}: ModeSelectionItemProps) {
|
}: ModeSelectionItemProps) {
|
||||||
const [checked, setChecked] = useState(currentMode == mode.key);
|
const [checked, setChecked] = useState(currentMode == mode.key);
|
||||||
@@ -109,7 +112,7 @@ export function ModeSelectionItem({
|
|||||||
{!isApproveModeConfigure && (mode.key == 'approve' || mode.key == 'smart_approve') && (
|
{!isApproveModeConfigure && (mode.key == 'approve' || mode.key == 'smart_approve') && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDislogOpen(true);
|
setView('permission');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Gear className="w-5 h-5 text-textSubtle hover:text-textStandard" />
|
<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">
|
<label className="block text-sm font-medium text-textStandard">
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-500 mb-2">
|
<p className="text-sm text-textSubtle mb-2">
|
||||||
{getFirstSentence(tool.description)}
|
{getFirstSentence(tool.description)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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