change configure approve mode in chat to open tool permissions dialog in a modal

This commit is contained in:
Zane Staggs
2025-07-14 14:54:56 -07:00
parent ecf6ab7fdd
commit 4b98ffc88e
9 changed files with 162 additions and 68 deletions
+1 -8
View File
@@ -1219,14 +1219,7 @@ export default function ChatInput({
</div>
</Tooltip>
<div className="w-px h-4 bg-border-default mx-2" />
<Tooltip>
<TooltipTrigger asChild>
<div>
<BottomMenuModeSelection setView={setView} />
</div>
</TooltipTrigger>
<TooltipContent>Current Goose mode configuration</TooltipContent>
</Tooltip>
<BottomMenuModeSelection />
{messages.length > 0 && (
<ManualSummarizeButton
messages={messages}
@@ -2,7 +2,6 @@ import { useEffect, useCallback, useState } from 'react';
import { Tornado } from 'lucide-react';
import { all_goose_modes, ModeSelectionItem } from '../settings/mode/ModeSelectionItem';
import { useConfig } from '../ConfigContext';
import { View, ViewOptions } from '../../App';
import {
DropdownMenu,
DropdownMenuContent,
@@ -10,11 +9,7 @@ import {
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
interface BottomMenuModeSelectionProps {
setView: (view: View, viewOptions?: ViewOptions) => void;
}
export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProps) => {
export const BottomMenuModeSelection = () => {
const [gooseMode, setGooseMode] = useState('auto');
const { read, upsert } = useConfig();
@@ -74,9 +69,6 @@ export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProp
currentMode={gooseMode}
showDescription={false}
isApproveModeConfigure={false}
parentView="chat"
parentViewOptions={undefined}
setView={setView}
handleModeChange={handleModeChange}
/>
</DropdownMenuItem>
@@ -110,7 +110,7 @@ export default function SettingsView({
value="chat"
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
>
<ChatSettingsSection setView={setView} />
<ChatSettingsSection />
</TabsContent>
<TabsContent
@@ -1,4 +1,3 @@
import { View, ViewOptions } from '../../../App';
import { ModeSection } from '../mode/ModeSection';
import { ToolSelectionStrategySection } from '../tool_selection_strategy/ToolSelectionStrategySection';
import SchedulerSection from '../scheduler/SchedulerSection';
@@ -6,11 +5,7 @@ import DictationSection from '../dictation/DictationSection';
import { ResponseStylesSection } from '../response_styles/ResponseStylesSection';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
interface ChatSettingsSectionProps {
setView: (view: View, viewOptions?: ViewOptions) => void;
}
export default function ChatSettingsSection({ setView }: ChatSettingsSectionProps) {
export default function ChatSettingsSection() {
return (
<div className="space-y-4 pr-4 pb-8 mt-1">
<Card className="pb-2 rounded-lg">
@@ -19,7 +14,7 @@ export default function ChatSettingsSection({ setView }: ChatSettingsSectionProp
<CardDescription>Configure how Goose interacts with tools and extensions</CardDescription>
</CardHeader>
<CardContent className="px-2">
<ModeSection setView={setView} />
<ModeSection />
</CardContent>
</Card>
@@ -64,7 +59,7 @@ export default function ChatSettingsSection({ setView }: ChatSettingsSectionProp
</CardDescription>
</CardHeader>
<CardContent className="px-2">
<ToolSelectionStrategySection setView={setView} />
<ToolSelectionStrategySection />
</CardContent>
</Card>
</div>
@@ -70,9 +70,6 @@ export function ConfigureApproveMode({
showDescription={true}
currentMode={approveMode || ''}
isApproveModeConfigure={true}
parentView={'settings' as const}
parentViewOptions={undefined}
setView={() => {}} // No-op since we're in configure mode
handleModeChange={(newMode) => {
setApproveMode(newMode);
}}
@@ -1,14 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem';
import { View, ViewOptions } from '../../../App';
import { useConfig } from '../../ConfigContext';
import { Input } from '../../ui/input';
interface ModeSectionProps {
setView: (view: View, viewOptions?: ViewOptions) => void;
}
export const ModeSection = ({ setView }: ModeSectionProps) => {
export const ModeSection = () => {
const [currentMode, setCurrentMode] = useState('auto');
const [maxTurns, setMaxTurns] = useState<number>(1000);
const { read, upsert } = useConfig();
@@ -68,9 +63,6 @@ export const ModeSection = ({ setView }: ModeSectionProps) => {
currentMode={currentMode}
showDescription={true}
isApproveModeConfigure={false}
parentView="settings"
parentViewOptions={{ section: 'chat' }}
setView={setView}
handleModeChange={handleModeChange}
/>
))}
@@ -1,7 +1,7 @@
import { useEffect, useState, forwardRef } from 'react';
import { Gear } from '../../icons';
import { ConfigureApproveMode } from './ConfigureApproveMode';
import { View, ViewOptions } from '../../../App';
import PermissionRulesModal from '../permission/PermissionRulesModal';
export interface GooseMode {
key: string;
@@ -37,28 +37,14 @@ interface ModeSelectionItemProps {
mode: GooseMode;
showDescription: boolean;
isApproveModeConfigure: boolean;
parentView: View;
parentViewOptions?: ViewOptions;
setView: (view: View, viewOptions?: ViewOptions) => void;
handleModeChange: (newMode: string) => void;
}
export const ModeSelectionItem = forwardRef<HTMLDivElement, ModeSelectionItemProps>(
(
{
currentMode,
mode,
showDescription,
isApproveModeConfigure,
parentView,
parentViewOptions,
setView,
handleModeChange,
},
ref
) => {
({ currentMode, mode, showDescription, isApproveModeConfigure, handleModeChange }, ref) => {
const [checked, setChecked] = useState(currentMode == mode.key);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isPermissionModalOpen, setIsPermissionModalOpen] = useState(false);
useEffect(() => {
setChecked(currentMode === mode.key);
@@ -80,11 +66,9 @@ export const ModeSelectionItem = forwardRef<HTMLDivElement, ModeSelectionItemPro
<div className="relative flex items-center gap-2">
{!isApproveModeConfigure && (mode.key == 'approve' || mode.key == 'smart_approve') && (
<button
onClick={() => {
setView('permission', {
parentView,
parentViewOptions,
});
onClick={(e) => {
e.stopPropagation(); // Prevent triggering the mode change
setIsPermissionModalOpen(true);
}}
>
<Gear className="w-4 h-4 text-text-muted hover:text-text-default" />
@@ -119,6 +103,12 @@ export const ModeSelectionItem = forwardRef<HTMLDivElement, ModeSelectionItemPro
) : null}
</div>
</div>
{/* Permission Rules Modal */}
<PermissionRulesModal
isOpen={isPermissionModalOpen}
onClose={() => setIsPermissionModalOpen(false)}
/>
</div>
);
}
@@ -0,0 +1,142 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
import { ScrollArea } from '../../ui/scroll-area';
import { FixedExtensionEntry, useConfig } from '../../ConfigContext';
import { ChevronRight } from 'lucide-react';
import PermissionModal from './PermissionModal';
import { Button } from '../../ui/button';
function RuleItem({ title, description }: { title: string; description: string }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleModalClose = () => {
setIsModalOpen(false);
};
return (
<>
<Button
className="flex items-center gap-2 w-full justify-between"
onClick={() => setIsModalOpen(true)}
variant="secondary"
size="lg"
>
<div>
<h3 className="font-semibold text-textStandard">{title}</h3>
<p className="text-xs text-textSubtle mt-1">{description}</p>
</div>
<ChevronRight className="w-4 h-4 text-iconStandard" />
</Button>
{isModalOpen && <PermissionModal onClose={handleModalClose} extensionName={title} />}
</>
);
}
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>
);
}
interface PermissionRulesModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function PermissionRulesModal({ isOpen, onClose }: PermissionRulesModalProps) {
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);
enabledExtensions.push({
name: 'platform',
type: 'builtin',
enabled: true,
});
// 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(() => {
if (isOpen) {
fetchExtensions();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[800px] max-h-[80vh] p-0">
<DialogHeader className="px-8 pt-6 pb-4">
<div className="flex items-center gap-4">
<div className="rounded-full bg-background-inverse w-16 h-16 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="stroke-text-inverse fill-background-inverse"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4" />
<path d="m21 2-9.6 9.6" />
<circle cx="7.5" cy="15.5" r="5.5" />
</svg>
</div>
<div>
<DialogTitle className="text-3xl font-medium text-textStandard">
Permission Rules
</DialogTitle>
<p className="text-textSubtle">
Configure tool permissions for extensions to control how they interact with your
system.
</p>
</div>
</div>
</DialogHeader>
<ScrollArea className="flex-1 px-8 pb-8">
<div className="space-y-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>
</ScrollArea>
</DialogContent>
</Dialog>
);
}
@@ -1,12 +1,7 @@
import { useEffect, useState, useCallback } from 'react';
import { View, ViewOptions } from '../../../App';
import { useConfig } from '../../ConfigContext';
import { getApiUrl, getSecretKey } from '../../../config';
interface ToolSelectionStrategySectionProps {
setView: (view: View, viewOptions?: ViewOptions) => void;
}
interface ToolSelectionStrategy {
key: string;
label: string;
@@ -32,9 +27,7 @@ export const all_tool_selection_strategies: ToolSelectionStrategy[] = [
},
];
export const ToolSelectionStrategySection = ({
setView: _setView,
}: ToolSelectionStrategySectionProps) => {
export const ToolSelectionStrategySection = () => {
const [currentStrategy, setCurrentStrategy] = useState('default');
const [_error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);