mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
change configure approve mode in chat to open tool permissions dialog in a modal
This commit is contained in:
@@ -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
-8
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user