Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 53 additions & 6 deletions ui/desktop/src/components/BottomMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Document, ChevronUp, ChevronDown } from './icons';
import type { View } from '../ChatWindow';
import { getApiUrl, getSecretKey } from '../config';
import { BottomMenuModeSelection } from './BottomMenuModeSelection';

export default function BottomMenu({
hasMessages,
Expand All @@ -15,11 +16,14 @@
setView: (view: View) => void;
}) {
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
const [gooseMode, setGooseMode] = useState('auto');
const { currentModel } = useModel();
const { recentModels } = useRecentModels(); // Get recent models

Check warning on line 20 in ui/desktop/src/components/BottomMenu.tsx

View workflow job for this annotation

GitHub Actions / Lint Electron Desktop App

'recentModels' is assigned a value but never used. Allowed unused vars must match /^_/u
const dropdownRef = useRef<HTMLDivElement>(null);

const [isGooseModeMenuOpen, setIsGooseModeMenuOpen] = useState(false);
const [gooseMode, setGooseMode] = useState('auto');
const gooseModeDropdownRef = useRef<HTMLDivElement>(null);

// Add effect to handle clicks outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
Expand Down Expand Up @@ -79,6 +83,41 @@
};
}, [isModelMenuOpen]);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
gooseModeDropdownRef.current &&
!gooseModeDropdownRef.current.contains(event.target as Node)
) {
setIsGooseModeMenuOpen(false);
}
};

if (isGooseModeMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
}

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isGooseModeMenuOpen]);

useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsGooseModeMenuOpen(false);
}
};

if (isGooseModeMenuOpen) {
window.addEventListener('keydown', handleEsc);
}

return () => {
window.removeEventListener('keydown', handleEsc);
};
}, [isGooseModeMenuOpen]);

let envModelProvider = null;
if (window.electron.getConfig().GOOSE_MODEL && window.electron.getConfig().GOOSE_PROVIDER) {
envModelProvider = `${window.electron.getConfig().GOOSE_MODEL} - ${window.electron.getConfig().GOOSE_PROVIDER}`;
Expand All @@ -90,7 +129,6 @@
<span
className="cursor-pointer flex items-center [&>svg]:size-4"
onClick={async () => {
console.log('Opening directory chooser');
if (hasMessages) {
window.electron.directoryChooser();
} else {
Expand All @@ -103,15 +141,24 @@
<ChevronUp className="ml-1" />
</span>

<div className="relative flex items-center ml-6">
{/* Goose Mode Selector Dropdown */}
<div className="relative flex items-center ml-6" ref={gooseModeDropdownRef}>
<div
className="flex items-center cursor-pointer"
onClick={() => {
setView('settings');
}}
onClick={() => setIsGooseModeMenuOpen(!isGooseModeMenuOpen)}
>
<span>Goose Mode: {gooseMode}</span>
{isGooseModeMenuOpen ? (
<ChevronDown className="w-4 h-4 ml-1" />
) : (
<ChevronUp className="w-4 h-4 ml-1" />
)}
</div>

{/* Dropdown Menu */}
{isGooseModeMenuOpen && (
<BottomMenuModeSelection selectedMode={gooseMode} setSelectedMode={setGooseMode} />
)}
</div>

{/* Model Selector Dropdown - Only in development */}
Expand Down
73 changes: 73 additions & 0 deletions ui/desktop/src/components/BottomMenuModeSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import { getApiUrl, getSecretKey } from '../config';

export const BottomMenuModeSelection = ({ selectedMode, setSelectedMode }) => {
const modes = [
{
value: 'auto',
},
{
value: 'approve',
},
{
value: 'chat',
},
];

const handleModeChange = async (newMode: string) => {
const storeResponse = await fetch(getApiUrl('/configs/store'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
body: JSON.stringify({
key: 'GOOSE_MODE',
value: newMode,
isSecret: false,
}),
});

if (!storeResponse.ok) {
const errorText = await storeResponse.text();
console.error('Store response error:', errorText);
throw new Error(`Failed to store new goose mode: ${newMode}`);
}
setSelectedMode(newMode);
};

return (
<div className="absolute bottom-[24px] right-0 w-[120px] bg-bgApp rounded-lg border border-borderSubtle">
<div>
{modes.map((mode) => (
<label key={mode.value} className="block cursor-pointer">
<div
className="flex items-center justify-between p-2 text-textStandard hover:bg-bgSubtle transition-colors"
onClick={() => handleModeChange(mode.value)}
>
<div>
<p className="text-sm">{mode.value}</p>
</div>
<div className="relative">
<input
type="radio"
name="modes"
value={mode.value}
checked={selectedMode === mode.value}
onChange={() => handleModeChange(mode.value)}
className="peer sr-only"
/>
<div
className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
peer-checked:bg-white dark:peer-checked:bg-black
transition-all duration-200 ease-in-out"
></div>
</div>
</div>
</label>
))}
</div>
</div>
);
};
Loading