refactor: Component hierarchy and code cleanup (#1319)

This commit is contained in:
Alex Hancock
2025-02-21 11:29:07 -05:00
committed by GitHub
parent bfe23765b5
commit 81a0334aa1
28 changed files with 475 additions and 823 deletions

View File

@@ -1,5 +0,0 @@
Put `goosey` in your $PATH if you want to launch via:
`goosey .`
Will open goose GUI from any path you specify

View File

@@ -0,0 +1,9 @@
# Goosey
Put `goosey` in your $PATH if you want to launch via:
```
goosey .
```
This will open goose GUI from any path you specify

View File

@@ -1,64 +1,146 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { addExtensionFromDeepLink } from './extensions'; import { addExtensionFromDeepLink } from './extensions';
import LauncherWindow from './LauncherWindow'; import { getStoredModel } from './utils/providerUtils';
import ChatWindow from './ChatWindow'; import { getStoredProvider, initializeSystem } from './utils/providerUtils';
import { useModel } from './components/settings/models/ModelContext';
import { useRecentModels } from './components/settings/models/RecentModels';
import { createSelectedModel } from './components/settings/models/utils';
import { getDefaultModel } from './components/settings/models/hardcoded_stuff';
import ErrorScreen from './components/ErrorScreen'; import ErrorScreen from './components/ErrorScreen';
import { ConfirmationModal } from './components/ui/ConfirmationModal'; import { ConfirmationModal } from './components/ui/ConfirmationModal';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import { ModelProvider } from './components/settings/models/ModelContext';
import { ActiveKeysProvider } from './components/settings/api_keys/ActiveKeysContext';
import { extractExtensionName } from './components/settings/extensions/utils'; import { extractExtensionName } from './components/settings/extensions/utils';
import WelcomeView from './components/WelcomeView';
import ChatView from './components/ChatView';
import SettingsView from './components/settings/SettingsView';
import MoreModelsView from './components/settings/models/MoreModelsView';
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
import 'react-toastify/dist/ReactToastify.css';
export type View =
| 'welcome'
| 'chat'
| 'settings'
| 'moreModels'
| 'configureProviders'
| 'configPage';
export default function App() { export default function App() {
const [fatalError, setFatalError] = useState<string | null>(null); const [fatalError, setFatalError] = useState<string | null>(null);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [pendingLink, setPendingLink] = useState<string | null>(null); const [pendingLink, setPendingLink] = useState<string | null>(null);
const [modalMessage, setModalMessage] = useState<string>(''); const [modalMessage, setModalMessage] = useState<string>('');
const [isInstalling, setIsInstalling] = useState(false); // Track installation progress const [isInstalling, setIsInstalling] = useState(false);
const searchParams = new URLSearchParams(window.location.search); const [view, setView] = useState<View>('welcome');
const isLauncher = searchParams.get('window') === 'launcher'; const { switchModel } = useModel();
const navigate = () => console.log('todo - bring back nav'); const { addRecentModel } = useRecentModels();
// Utility function to extract the command from the link // Utility function to extract the command from the link
function extractCommand(link: string): string { function extractCommand(link: string): string {
const url = new URL(link); const url = new URL(link);
const cmd = url.searchParams.get('cmd') || 'Unknown Command'; const cmd = url.searchParams.get('cmd') || 'Unknown Command';
const args = url.searchParams.getAll('arg').map(decodeURIComponent); const args = url.searchParams.getAll('arg').map(decodeURIComponent);
return `${cmd} ${args.join(' ')}`.trim(); // Combine the command and arguments return `${cmd} ${args.join(' ')}`.trim();
} }
useEffect(() => { useEffect(() => {
const handleAddExtension = (_, link: string) => { const handleAddExtension = (_: any, link: string) => {
const command = extractCommand(link); // Extract and format the command const command = extractCommand(link);
const extName = extractExtensionName(link); const extName = extractExtensionName(link);
window.electron.logInfo(`Adding extension from deep link ${link}`); window.electron.logInfo(`Adding extension from deep link ${link}`);
setPendingLink(link); // Save the link for later use setPendingLink(link);
setModalMessage( setModalMessage(
`Are you sure you want to install the ${extName} extension?\n\nCommand: ${command}` `Are you sure you want to install the ${extName} extension?\n\nCommand: ${command}`
); // Display command );
setModalVisible(true); // Show confirmation modal setModalVisible(true);
}; };
window.electron.on('add-extension', handleAddExtension); window.electron.on('add-extension', handleAddExtension);
return () => { return () => {
// Clean up the event listener when the component unmounts
window.electron.off('add-extension', handleAddExtension); window.electron.off('add-extension', handleAddExtension);
}; };
}, []); }, []);
// Keyboard shortcut handler
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'n') {
event.preventDefault();
window.electron.createChatWindow();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
// Attempt to detect config for a stored provider
useEffect(() => {
const config = window.electron.getConfig();
const storedProvider = getStoredProvider(config);
if (storedProvider) {
setView('chat');
} else {
setView('welcome');
}
}, []);
// Initialize system if we have a stored provider
useEffect(() => {
const setupStoredProvider = async () => {
const config = window.electron.getConfig();
if (config.GOOSE_PROVIDER && config.GOOSE_MODEL) {
window.electron.logInfo(
'Initializing system with environment: GOOSE_MODEL and GOOSE_PROVIDER as priority.'
);
await initializeSystem(config.GOOSE_PROVIDER, config.GOOSE_MODEL);
return;
}
const storedProvider = getStoredProvider(config);
const storedModel = getStoredModel();
if (storedProvider) {
try {
await initializeSystem(storedProvider, storedModel);
if (!storedModel) {
const modelName = getDefaultModel(storedProvider.toLowerCase());
const model = createSelectedModel(storedProvider.toLowerCase(), modelName);
switchModel(model);
addRecentModel(model);
}
} catch (error) {
console.error('Failed to initialize with stored provider:', error);
}
}
};
setupStoredProvider();
}, []);
useEffect(() => {
const handleFatalError = (_: any, errorMessage: string) => {
setFatalError(errorMessage);
};
window.electron.on('fatal-error', handleFatalError);
return () => {
window.electron.off('fatal-error', handleFatalError);
};
}, []);
const handleConfirm = async () => { const handleConfirm = async () => {
if (pendingLink && !isInstalling) { if (pendingLink && !isInstalling) {
setIsInstalling(true); // Disable further attempts setIsInstalling(true);
console.log('Confirming installation for link:', pendingLink);
try { try {
await addExtensionFromDeepLink(pendingLink, navigate); await addExtensionFromDeepLink(pendingLink, setView);
} catch (error) { } catch (error) {
console.error('Failed to add extension:', error); console.error('Failed to add extension:', error);
} finally { } finally {
// Always reset states
setModalVisible(false); setModalVisible(false);
setPendingLink(null); setPendingLink(null);
setIsInstalling(false); setIsInstalling(false);
@@ -69,28 +151,22 @@ export default function App() {
const handleCancel = () => { const handleCancel = () => {
console.log('Cancelled extension installation.'); console.log('Cancelled extension installation.');
setModalVisible(false); setModalVisible(false);
setPendingLink(null); // Clear the link if the user cancels setPendingLink(null);
}; };
useEffect(() => {
const handleFatalError = (_: any, errorMessage: string) => {
setFatalError(errorMessage);
};
// Listen for fatal errors from main process
window.electron.on('fatal-error', handleFatalError);
return () => {
window.electron.off('fatal-error', handleFatalError);
};
}, []);
if (fatalError) { if (fatalError) {
return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />; return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />;
} }
return ( return (
<> <>
<ToastContainer
aria-label="Toast notifications"
position="top-right"
autoClose={3000}
closeOnClick
pauseOnHover
/>
{modalVisible && ( {modalVisible && (
<ConfirmationModal <ConfirmationModal
isOpen={modalVisible} isOpen={modalVisible}
@@ -101,18 +177,42 @@ export default function App() {
isSubmitting={isInstalling} isSubmitting={isInstalling}
/> />
)} )}
<ModelProvider> <div className="relative w-screen h-screen overflow-hidden bg-bgApp flex flex-col">
<ActiveKeysProvider> <div className="titlebar-drag-region" />
{isLauncher ? <LauncherWindow /> : <ChatWindow />} <div>
<ToastContainer {view === 'welcome' && (
aria-label="Toast notifications" <WelcomeView
position="top-right" onSubmit={() => {
autoClose={3000} setView('chat');
closeOnClick }}
pauseOnHover
/> />
</ActiveKeysProvider> )}
</ModelProvider> {view === 'settings' && (
<SettingsView
onClose={() => {
setView('chat');
}}
setView={setView}
/>
)}
{view === 'moreModels' && (
<MoreModelsView
onClose={() => {
setView('settings');
}}
setView={setView}
/>
)}
{view === 'configureProviders' && (
<ConfigureProvidersView
onClose={() => {
setView('settings');
}}
/>
)}
{view === 'chat' && <ChatView setView={setView} />}
</div>
</div>
</> </>
); );
} }

View File

@@ -1,414 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { Message, useChat } from './ai-sdk-fork/useChat';
import { getApiUrl, getSecretKey } from './config';
import BottomMenu from './components/BottomMenu';
import FlappyGoose from './components/FlappyGoose';
import GooseMessage from './components/GooseMessage';
import Input from './components/Input';
import LoadingGoose from './components/LoadingGoose';
import MoreMenu from './components/MoreMenu';
import { Card } from './components/ui/card';
import { ScrollArea, ScrollAreaHandle } from './components/ui/scroll-area';
import UserMessage from './components/UserMessage';
import WingToWing, { Working } from './components/WingToWing';
import { askAi } from './utils/askAI';
import { getStoredModel, Provider } from './utils/providerUtils';
import { ChatLayout } from './components/chat_window/ChatLayout';
import { WelcomeScreen } from './components/welcome_screen/WelcomeScreen';
import { getStoredProvider, initializeSystem } from './utils/providerUtils';
import { useModel } from './components/settings/models/ModelContext';
import { useRecentModels } from './components/settings/models/RecentModels';
import { createSelectedModel } from './components/settings/models/utils';
import { getDefaultModel } from './components/settings/models/hardcoded_stuff';
import Splash from './components/Splash';
import Settings from './components/settings/Settings';
import MoreModelsSettings from './components/settings/models/MoreModels';
import ConfigureProviders from './components/settings/providers/ConfigureProviders';
import { ConfigPage } from './components/pages/ConfigPage';
export interface Chat {
id: number;
title: string;
messages: Array<{
id: string;
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool';
content: string;
}>;
}
export type View =
| 'welcome'
| 'chat'
| 'settings'
| 'moreModels'
| 'configureProviders'
| 'configPage';
// This component is our main chat content.
// We'll move the majority of chat logic here, minus the 'view' state.
export function ChatContent({
chats,
setChats,
selectedChatId,
setSelectedChatId,
initialQuery,
setProgressMessage,
setWorking,
setView,
}: {
chats: Chat[];
setChats: React.Dispatch<React.SetStateAction<Chat[]>>;
selectedChatId: number;
setSelectedChatId: React.Dispatch<React.SetStateAction<number>>;
initialQuery: string | null;
setProgressMessage: React.Dispatch<React.SetStateAction<string>>;
setWorking: React.Dispatch<React.SetStateAction<Working>>;
setView: (view: View) => void;
}) {
const chat = chats.find((c: Chat) => c.id === selectedChatId);
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
const [showGame, setShowGame] = useState(false);
const [working, setWorkingLocal] = useState<Working>(Working.Idle);
const scrollRef = useRef<ScrollAreaHandle>(null);
useEffect(() => {
setWorking(working);
}, [working, setWorking]);
const updateWorking = (newWorking: Working) => {
setWorkingLocal(newWorking);
};
const { messages, append, stop, isLoading, error, setMessages } = useChat({
api: getApiUrl('/reply'),
initialMessages: chat?.messages || [],
onToolCall: ({ toolCall }) => {
updateWorking(Working.Working);
setProgressMessage(`Executing tool: ${toolCall.toolName}`);
},
onResponse: (response) => {
if (!response.ok) {
setProgressMessage('An error occurred while receiving the response.');
updateWorking(Working.Idle);
} else {
setProgressMessage('thinking...');
updateWorking(Working.Working);
}
},
onFinish: async (message, _) => {
window.electron.stopPowerSaveBlocker();
setTimeout(() => {
setProgressMessage('Task finished. Click here to expand.');
updateWorking(Working.Idle);
}, 500);
const fetchResponses = await askAi(message.content);
setMessageMetadata((prev) => ({ ...prev, [message.id]: fetchResponses }));
const timeSinceLastInteraction = Date.now() - lastInteractionTime;
window.electron.logInfo('last interaction:' + lastInteractionTime);
if (timeSinceLastInteraction > 60000) {
// 60000ms = 1 minute
window.electron.showNotification({
title: 'Goose finished the task.',
body: 'Click here to expand.',
});
}
},
});
// Update chat messages when they change
useEffect(() => {
const updatedChats = chats.map((c) => (c.id === selectedChatId ? { ...c, messages } : c));
setChats(updatedChats);
}, [messages, selectedChatId]);
const initialQueryAppended = useRef(false);
useEffect(() => {
if (initialQuery && !initialQueryAppended.current) {
append({ role: 'user', content: initialQuery });
initialQueryAppended.current = true;
}
}, [initialQuery]);
useEffect(() => {
if (messages.length > 0) {
setHasMessages(true);
}
}, [messages]);
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
window.electron.startPowerSaveBlocker();
const customEvent = e as CustomEvent;
const content = customEvent.detail?.value || '';
if (content.trim()) {
setLastInteractionTime(Date.now());
append({
role: 'user',
content,
});
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
}
};
if (error) {
console.log('Error:', error);
}
const onStopGoose = () => {
stop();
setLastInteractionTime(Date.now());
window.electron.stopPowerSaveBlocker();
const lastMessage: Message = messages[messages.length - 1];
if (lastMessage.role === 'user' && lastMessage.toolInvocations === undefined) {
// Remove the last user message.
if (messages.length > 1) {
setMessages(messages.slice(0, -1));
} else {
setMessages([]);
}
} else if (lastMessage.role === 'assistant' && lastMessage.toolInvocations !== undefined) {
// Add messaging about interrupted ongoing tool invocations
const newLastMessage: Message = {
...lastMessage,
toolInvocations: lastMessage.toolInvocations.map((invocation) => {
if (invocation.state !== 'result') {
return {
...invocation,
result: [
{
audience: ['user'],
text: 'Interrupted.\n',
type: 'text',
},
{
audience: ['assistant'],
text: 'Interrupted by the user to make a correction.\n',
type: 'text',
},
],
state: 'result',
};
} else {
return invocation;
}
}),
};
const updatedMessages = [...messages.slice(0, -1), newLastMessage];
setMessages(updatedMessages);
}
};
return (
<div className="flex flex-col w-full h-screen items-center justify-center">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle border-b border-borderSubtle">
{/* Pass setView to MoreMenu so it can switch to settings or other views */}
<MoreMenu setView={setView} />
</div>
<Card className="flex flex-col flex-1 rounded-none h-[calc(100vh-95px)] w-full bg-bgApp mt-0 border-none relative">
{messages.length === 0 ? (
<Splash append={append} />
) : (
<ScrollArea ref={scrollRef} className="flex-1 px-4" autoScroll>
{messages.map((message) => (
<div key={message.id} className="mt-[16px]">
{message.role === 'user' ? (
<UserMessage message={message} />
) : (
<GooseMessage
message={message}
messages={messages}
metadata={messageMetadata[message.id]}
append={append}
/>
)}
</div>
))}
{error && (
<div className="flex flex-col items-center justify-center p-4">
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-3 rounded-lg mb-2">
{error.message || 'Honk! Goose experienced an error while responding'}
{error.status && <span className="ml-2">(Status: {error.status})</span>}
</div>
<div
className="px-3 py-2 mt-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
onClick={async () => {
const lastUserMessage = messages.reduceRight(
(found, m) => found || (m.role === 'user' ? m : null),
null
);
if (lastUserMessage) {
append({
role: 'user',
content: lastUserMessage.content,
});
}
}}
>
Retry Last Message
</div>
</div>
)}
<div className="block h-16" />
</ScrollArea>
)}
<div className="relative">
{isLoading && <LoadingGoose />}
<Input
handleSubmit={handleSubmit}
disabled={isLoading}
isLoading={isLoading}
onStop={onStopGoose}
/>
<BottomMenu hasMessages={hasMessages} setView={setView} />
</div>
</Card>
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
</div>
);
}
export default function ChatWindow() {
// We'll add a state controlling which "view" is active.
const [view, setView] = useState<View>('welcome');
// Shared function to create a chat window
const openNewChatWindow = () => {
window.electron.createChatWindow();
};
const { switchModel } = useModel();
const { addRecentModel } = useRecentModels();
// This will store chat data for the "chat" view.
const [chats, setChats] = useState<Chat[]>(() => [
{
id: 1,
title: 'Chat 1',
messages: [],
},
]);
const [selectedChatId, setSelectedChatId] = useState(1);
// Additional states
const [mode, setMode] = useState<'expanded' | 'compact'>('expanded');
const [working, setWorking] = useState<Working>(Working.Idle);
const [progressMessage, setProgressMessage] = useState<string>('');
const [initialQuery, setInitialQuery] = useState<string | null>(null);
// Keyboard shortcut handler
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'n') {
event.preventDefault();
openNewChatWindow();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
// Attempt to detect config for a stored provider
useEffect(() => {
const config = window.electron.getConfig();
const storedProvider = getStoredProvider(config);
if (storedProvider) {
setView('chat');
} else {
setView('welcome');
}
}, []);
// Initialize system if we have a stored provider
useEffect(() => {
const setupStoredProvider = async () => {
const config = window.electron.getConfig();
if (config.GOOSE_PROVIDER && config.GOOSE_MODEL) {
window.electron.logInfo(
'Initializing system with environment: GOOSE_MODEL and GOOSE_PROVIDER as priority.'
);
await initializeSystem(config.GOOSE_PROVIDER, config.GOOSE_MODEL);
return;
}
const storedProvider = getStoredProvider(config);
const storedModel = getStoredModel();
if (storedProvider) {
try {
await initializeSystem(storedProvider, storedModel);
if (!storedModel) {
const modelName = getDefaultModel(storedProvider.toLowerCase());
const model = createSelectedModel(storedProvider.toLowerCase(), modelName);
switchModel(model);
addRecentModel(model);
}
} catch (error) {
console.error('Failed to initialize with stored provider:', error);
}
}
};
setupStoredProvider();
}, []);
return (
<ChatLayout mode={mode}>
{/* Conditionally render based on `view` */}
{view === 'welcome' && (
<WelcomeScreen
onSubmit={() => {
setView('chat');
}}
/>
)}
{view === 'settings' && (
<Settings
onClose={() => {
setView('chat');
}}
setView={setView}
/>
)}
{view === 'moreModels' && (
<MoreModelsSettings
onClose={() => {
setView('settings');
}}
setView={setView}
/>
)}
{view === 'configureProviders' && (
<ConfigureProviders
onClose={() => {
setView('settings');
}}
setView={setView}
/>
)}
{view === 'chat' && (
<ChatContent
chats={chats}
setChats={setChats}
selectedChatId={selectedChatId}
setSelectedChatId={setSelectedChatId}
initialQuery={initialQuery}
setProgressMessage={setProgressMessage}
setWorking={setWorking}
setView={setView}
/>
)}
</ChatLayout>
);
}

View File

@@ -1,35 +0,0 @@
import React, { useRef, useState } from 'react';
export default function SpotlightWindow() {
const [query, setQuery] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) {
// Create a new chat window with the query
window.electron.createChatWindow(query);
setQuery('');
inputRef.current.blur();
}
};
return (
<div className="h-screen w-screen flex items-center justify-center bg-transparent overflow-hidden">
<form
onSubmit={handleSubmit}
className="w-[600px] bg-white/80 backdrop-blur-lg rounded-lg shadow-lg p-4"
>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full bg-transparent text-black text-xl px-4 py-2 outline-none placeholder-gray-400"
placeholder="Type a command..."
autoFocus
/>
</form>
</div>
);
}

View File

@@ -0,0 +1,209 @@
import React, { useEffect, useRef, useState } from 'react';
import { Message, useChat } from '../ai-sdk-fork/useChat';
import { getApiUrl } from '../config';
import BottomMenu from './BottomMenu';
import FlappyGoose from './FlappyGoose';
import GooseMessage from './GooseMessage';
import Input from './Input';
import { type View } from '../App';
import LoadingGoose from './LoadingGoose';
import MoreMenu from './MoreMenu';
import { Card } from './ui/card';
import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
import UserMessage from './UserMessage';
import { askAi } from '../utils/askAI';
import Splash from './Splash';
import 'react-toastify/dist/ReactToastify.css';
export interface ChatType {
id: number;
title: string;
messages: Array<{
id: string;
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool';
content: string;
}>;
}
export default function ChatView({ setView }: { setView: (view: View) => void }) {
const [chat, setChat] = useState<ChatType>(() => {
return {
id: 1,
title: 'Chat 1',
messages: [],
};
});
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
const [showGame, setShowGame] = useState(false);
const scrollRef = useRef<ScrollAreaHandle>(null);
const { messages, append, stop, isLoading, error, setMessages } = useChat({
api: getApiUrl('/reply'),
initialMessages: chat?.messages || [],
onFinish: async (message, _) => {
window.electron.stopPowerSaveBlocker();
const fetchResponses = await askAi(message.content);
setMessageMetadata((prev) => ({ ...prev, [message.id]: fetchResponses }));
const timeSinceLastInteraction = Date.now() - lastInteractionTime;
window.electron.logInfo('last interaction:' + lastInteractionTime);
if (timeSinceLastInteraction > 60000) {
// 60000ms = 1 minute
window.electron.showNotification({
title: 'Goose finished the task.',
body: 'Click here to expand.',
});
}
},
});
// Update chat messages when they change
useEffect(() => {
setChat({ ...chat, messages });
}, [messages]);
useEffect(() => {
if (messages.length > 0) {
setHasMessages(true);
}
}, [messages]);
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
window.electron.startPowerSaveBlocker();
const customEvent = e as CustomEvent;
const content = customEvent.detail?.value || '';
if (content.trim()) {
setLastInteractionTime(Date.now());
append({
role: 'user',
content,
});
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
}
};
if (error) {
console.log('Error:', error);
}
const onStopGoose = () => {
stop();
setLastInteractionTime(Date.now());
window.electron.stopPowerSaveBlocker();
const lastMessage: Message = messages[messages.length - 1];
if (lastMessage.role === 'user' && lastMessage.toolInvocations === undefined) {
// Remove the last user message.
if (messages.length > 1) {
setMessages(messages.slice(0, -1));
} else {
setMessages([]);
}
} else if (lastMessage.role === 'assistant' && lastMessage.toolInvocations !== undefined) {
// Add messaging about interrupted ongoing tool invocations
const newLastMessage: Message = {
...lastMessage,
toolInvocations: lastMessage.toolInvocations.map((invocation) => {
if (invocation.state !== 'result') {
return {
...invocation,
result: [
{
audience: ['user'],
text: 'Interrupted.\n',
type: 'text',
},
{
audience: ['assistant'],
text: 'Interrupted by the user to make a correction.\n',
type: 'text',
},
],
state: 'result',
};
} else {
return invocation;
}
}),
};
const updatedMessages = [...messages.slice(0, -1), newLastMessage];
setMessages(updatedMessages);
}
};
return (
<div className="flex flex-col w-full h-screen items-center justify-center">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle border-b border-borderSubtle">
<MoreMenu setView={setView} />
</div>
<Card className="flex flex-col flex-1 rounded-none h-[calc(100vh-95px)] w-full bg-bgApp mt-0 border-none relative">
{messages.length === 0 ? (
<Splash append={append} />
) : (
<ScrollArea ref={scrollRef} className="flex-1 px-4" autoScroll>
{messages.map((message) => (
<div key={message.id} className="mt-[16px]">
{message.role === 'user' ? (
<UserMessage message={message} />
) : (
<GooseMessage
message={message}
messages={messages}
metadata={messageMetadata[message.id]}
append={append}
/>
)}
</div>
))}
{error && (
<div className="flex flex-col items-center justify-center p-4">
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-3 rounded-lg mb-2">
{error.message || 'Honk! Goose experienced an error while responding'}
{error.status && <span className="ml-2">(Status: {error.status})</span>}
</div>
<div
className="px-3 py-2 mt-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
onClick={async () => {
const lastUserMessage = messages.reduceRight(
(found, m) => found || (m.role === 'user' ? m : null),
null
);
if (lastUserMessage) {
append({
role: 'user',
content: lastUserMessage.content,
});
}
}}
>
Retry Last Message
</div>
</div>
)}
<div className="block h-16" />
</ScrollArea>
)}
<div className="relative">
{isLoading && <LoadingGoose />}
<Input
handleSubmit={handleSubmit}
disabled={isLoading}
isLoading={isLoading}
onStop={onStopGoose}
/>
<BottomMenu hasMessages={hasMessages} setView={setView} />
</div>
</Card>
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
</div>
);
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
// Capture unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
window.electron.logInfo(`[UNHANDLED REJECTION] ${event.reason}`);
});
// Capture global errors
window.addEventListener('error', (event) => {
window.electron.logInfo(
`[GLOBAL ERROR] ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`
);
});
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(_: Error) {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Send error to main process
window.electron.logInfo(`[ERROR] ${error.toString()}\n${errorInfo.componentStack}`);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}

View File

@@ -1,14 +1,14 @@
import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '@radix-ui/react-popover'; import { Popover, PopoverContent, PopoverTrigger, PopoverPortal } from '@radix-ui/react-popover';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { More } from './icons'; import { More } from './icons';
import type { View } from '../../ChatWindow'; import type { View } from '../ChatWindow';
interface VersionInfo { interface VersionInfo {
current_version: string; current_version: string;
available_versions: string[]; available_versions: string[];
} }
// Accept setView as a prop from the parent (e.g. ChatContent) // Accept setView as a prop from the parent (e.g. Chat)
export default function MoreMenu({ setView }: { setView: (view: View) => void }) { export default function MoreMenu({ setView }: { setView: (view: View) => void }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [versions, setVersions] = useState<VersionInfo | null>(null); const [versions, setVersions] = useState<VersionInfo | null>(null);

View File

@@ -3,18 +3,18 @@ import {
supported_providers, supported_providers,
required_keys, required_keys,
provider_aliases, provider_aliases,
} from '../settings/models/hardcoded_stuff'; } from './settings/models/hardcoded_stuff';
import { useActiveKeys } from '../settings/api_keys/ActiveKeysContext'; import { useActiveKeys } from './settings/api_keys/ActiveKeysContext';
import { ProviderSetupModal } from '../settings/ProviderSetupModal'; import { ProviderSetupModal } from './settings/ProviderSetupModal';
import { useModel } from '../settings/models/ModelContext'; import { useModel } from './settings/models/ModelContext';
import { useRecentModels } from '../settings/models/RecentModels'; import { useRecentModels } from './settings/models/RecentModels';
import { createSelectedModel } from '../settings/models/utils'; import { createSelectedModel } from './settings/models/utils';
import { getDefaultModel } from '../settings/models/hardcoded_stuff'; import { getDefaultModel } from './settings/models/hardcoded_stuff';
import { initializeSystem } from '../../utils/providerUtils'; import { initializeSystem } from '../utils/providerUtils';
import { getApiUrl, getSecretKey } from '../../config'; import { getApiUrl, getSecretKey } from '../config';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { getActiveProviders, isSecretKey } from '../settings/api_keys/utils'; import { getActiveProviders, isSecretKey } from './settings/api_keys/utils';
import { BaseProviderGrid, getProviderDescription } from '../settings/providers/BaseProviderGrid'; import { BaseProviderGrid, getProviderDescription } from './settings/providers/BaseProviderGrid';
interface ProviderGridProps { interface ProviderGridProps {
onSubmit?: () => void; onSubmit?: () => void;

View File

@@ -1,45 +0,0 @@
import React from 'react';
import { Card } from './ui/card';
import { Bird } from './ui/icons';
interface WelcomeScreenProps {
className?: string;
onDismiss: () => void;
}
export function WelcomeScreen({ className, onDismiss }: WelcomeScreenProps) {
return (
<Card
className={`flex flex-col items-center justify-center p-8 space-y-6 bg-bgApp w-full h-full ${className}`}
>
<div className="w-16 h-16">
<Bird />
</div>
<div className="text-center space-y-4">
<h2 className="text-2xl font-semibold text-gray-800 dark:text-white/70">
Welcome to Goose 1.0 <b>beta</b>! 🎉
</h2>
<div className="whitespace-pre-wrap text-gray-600 dark:text-white/50">
Goose is your AI-powered agent.
<br />
<br />
<b>
{' '}
Warning: During the beta, your chats are not saved - closing the window <br />
or closing the app will lose your history. <br />
</b>
<br />
<br />
Try +N for a new window, or +O to work on a specific directory.
</div>
<button
onClick={onDismiss}
className="mt-6 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
>
Get Started
</button>
</div>
</Card>
);
}

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { ProviderGrid } from './ProviderGrid'; import { ProviderGrid } from './ProviderGrid';
import { ScrollArea } from '../ui/scroll-area'; import { ScrollArea } from './ui/scroll-area';
import { Button } from '../ui/button'; import { Button } from './ui/button';
import WelcomeGooseLogo from '../WelcomeGooseLogo'; import WelcomeGooseLogo from './WelcomeGooseLogo';
// Extending React CSSProperties to include custom webkit property // Extending React CSSProperties to include custom webkit property
declare module 'react' { declare module 'react' {
@@ -15,7 +15,7 @@ interface WelcomeScreenProps {
onSubmit?: () => void; onSubmit?: () => void;
} }
export function WelcomeScreen({ onSubmit }: WelcomeScreenProps) { export default function WelcomeScreen({ onSubmit }: WelcomeScreenProps) {
return ( return (
<div className="h-screen w-full select-none bg-white dark:bg-black"> <div className="h-screen w-full select-none bg-white dark:bg-black">
{/* Draggable title bar region */} {/* Draggable title bar region */}

View File

@@ -1,35 +0,0 @@
import React from 'react';
import { Bird } from '../components/ui/icons';
export enum Working {
Idle = 'Idle',
Working = 'Working',
}
interface WingToWingProps {
onExpand: () => void;
progressMessage: string;
working: Working;
}
const WingToWing: React.FC<WingToWingProps> = ({ onExpand, progressMessage, working }) => {
return (
<div
onClick={onExpand}
className="flex items-center w-full h-28 bg-gradient-to-r from-gray-100 via-gray-200 to-gray-300 shadow-md rounded-lg p-4 cursor-pointer hover:shadow-lg transition-all duration-200"
>
{working === Working.Working && (
<div className="w-10 h-10 mr-4 flex-shrink-0">
<Bird />
</div>
)}
{/* Status Text */}
<div className="flex flex-col text-left">
<span className="text-sm text-gray-600 font-medium">{progressMessage}</span>
</div>
</div>
);
};
export default WingToWing;

View File

@@ -1,8 +0,0 @@
import React from 'react';
export const ChatLayout = ({ children, mode }) => (
<div className="relative w-screen h-screen overflow-hidden bg-bgApp flex flex-col">
<div className="titlebar-drag-region" />
<div style={{ display: mode === 'expanded' ? 'block' : 'none' }}>{children}</div>
</div>
);

View File

@@ -14,7 +14,7 @@ import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExt
import BackButton from '../ui/BackButton'; import BackButton from '../ui/BackButton';
import { RecentModelsRadio } from './models/RecentModels'; import { RecentModelsRadio } from './models/RecentModels';
import { ExtensionItem } from './extensions/ExtensionItem'; import { ExtensionItem } from './extensions/ExtensionItem';
import type { View } from '../../ChatWindow'; import type { View } from '../../App';
const EXTENSIONS_DESCRIPTION = const EXTENSIONS_DESCRIPTION =
'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.'; 'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.';
@@ -46,10 +46,7 @@ const DEFAULT_SETTINGS: SettingsType = {
extensions: BUILT_IN_EXTENSIONS, extensions: BUILT_IN_EXTENSIONS,
}; };
// We'll accept two props: export default function SettingsView({
// onClose: to go back to chat
// setView: to switch to moreModels, configureProviders, etc.
export default function Settings({
onClose, onClose,
setView, setView,
}: { }: {

View File

@@ -3,20 +3,17 @@ import { RecentModels } from './RecentModels';
import { ProviderButtons } from './ProviderButtons'; import { ProviderButtons } from './ProviderButtons';
import BackButton from '../../ui/BackButton'; import BackButton from '../../ui/BackButton';
import { SearchBar } from './Search'; import { SearchBar } from './Search';
import { useModel } from './ModelContext';
import { AddModelInline } from './AddModelInline'; import { AddModelInline } from './AddModelInline';
import { ScrollArea } from '../../ui/scroll-area'; import { ScrollArea } from '../../ui/scroll-area';
import type { View } from '../../../ChatWindow'; import type { View } from '../../../App';
export default function MoreModelsPage({ export default function MoreModelsView({
onClose, onClose,
setView, setView,
}: { }: {
onClose: () => void; onClose: () => void;
setView: (view: View) => void; setView: (view: View) => void;
}) { }) {
const { currentModel } = useModel();
return ( return (
<div className="h-screen w-full"> <div className="h-screen w-full">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div> <div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>

View File

@@ -2,15 +2,8 @@ import React from 'react';
import { ScrollArea } from '../../ui/scroll-area'; import { ScrollArea } from '../../ui/scroll-area';
import BackButton from '../../ui/BackButton'; import BackButton from '../../ui/BackButton';
import { ConfigureProvidersGrid } from './ConfigureProvidersGrid'; import { ConfigureProvidersGrid } from './ConfigureProvidersGrid';
import type { View } from '../../../ChatWindow';
export default function ConfigureProviders({ export default function ConfigureProvidersView({ onClose }: { onClose: () => void }) {
onClose,
setView,
}: {
onClose: () => void;
setView?: (view: View) => void;
}) {
return ( return (
<div className="h-screen w-full"> <div className="h-screen w-full">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div> <div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>

View File

@@ -1,4 +1,5 @@
import { getApiUrl, getSecretKey } from './config'; import { getApiUrl, getSecretKey } from './config';
import { type View } from './App';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
// ExtensionConfig type matching the Rust version // ExtensionConfig type matching the Rust version
@@ -257,7 +258,7 @@ function handleError(message: string, shouldThrow = false): void {
} }
} }
export async function addExtensionFromDeepLink(url: string, navigate: any) { export async function addExtensionFromDeepLink(url: string, setView: (view: View) => void) {
if (!url.startsWith('goose://extension')) { if (!url.startsWith('goose://extension')) {
handleError( handleError(
'Failed to install extension: Invalid URL: URL must use the goose://extension scheme' 'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
@@ -343,7 +344,9 @@ export async function addExtensionFromDeepLink(url: string, navigate: any) {
// Check if extension requires env vars and go to settings if so // Check if extension requires env vars and go to settings if so
if (envVarsRequired(config)) { if (envVarsRequired(config)) {
console.log('Environment variables required, redirecting to settings'); console.log('Environment variables required, redirecting to settings');
navigate(`/settings?extensionId=${config.id}&showEnvVars=true`); setView('settings');
// TODO - add code which can auto-open the modal on the settings view
// navigate(`/settings?extensionId=${config.id}&showEnvVars=true`);
return; return;
} }

View File

@@ -181,47 +181,6 @@ let appConfig = {
secretKey: generateSecretKey(), secretKey: generateSecretKey(),
}; };
const createLauncher = () => {
const launcherWindow = new BrowserWindow({
width: 600,
height: 60,
frame: process.platform === 'darwin' ? false : true,
transparent: false,
webPreferences: {
preload: path.join(__dirname, 'preload.ts'),
additionalArguments: [JSON.stringify(appConfig)],
partition: 'persist:goose',
},
skipTaskbar: true,
alwaysOnTop: true,
});
// Center on screen
const primaryDisplay = electron.screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize;
const windowBounds = launcherWindow.getBounds();
launcherWindow.setPosition(
Math.round(width / 2 - windowBounds.width / 2),
Math.round(height / 3 - windowBounds.height / 2)
);
// Load launcher window content
const launcherParams = '?window=launcher#/launcher';
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
launcherWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${launcherParams}`);
} else {
launcherWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html${launcherParams}`)
);
}
// Destroy window when it loses focus
launcherWindow.on('blur', () => {
launcherWindow.destroy();
});
};
// Track windows by ID // Track windows by ID
let windowCounter = 0; let windowCounter = 0;
const windowMap = new Map<number, BrowserWindow>(); const windowMap = new Map<number, BrowserWindow>();
@@ -487,9 +446,6 @@ app.whenReady().then(async () => {
let openDir = dirPath || (recentDirs.length > 0 ? recentDirs[0] : null); let openDir = dirPath || (recentDirs.length > 0 ? recentDirs[0] : null);
createChat(app, undefined, openDir); createChat(app, undefined, openDir);
// Show launcher input on key combo
globalShortcut.register('Control+Alt+Command+G', createLauncher);
// Get the existing menu // Get the existing menu
const menu = Menu.getApplicationMenu(); const menu = Menu.getApplicationMenu();

View File

@@ -1,29 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron')
const config = JSON.parse(process.argv.find((arg) => arg.startsWith('{')) || '{}');
contextBridge.exposeInMainWorld('appConfig', {
get: (key) => config[key],
getAll: () => config,
});
contextBridge.exposeInMainWorld('electron', {
getConfig: () => config,
hideWindow: () => ipcRenderer.send('hide-window'),
directoryChooser: (replace) => ipcRenderer.send('directory-chooser', replace),
createChatWindow: (query, dir, version) => ipcRenderer.send('create-chat-window', query, dir, version),
logInfo: (txt) => ipcRenderer.send('logInfo', txt),
showNotification: (data) => ipcRenderer.send('notify', data),
createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query),
openInChrome: (url) => ipcRenderer.send('open-in-chrome', url),
fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url),
reloadApp: () => ipcRenderer.send('reload-app'),
checkForOllama: () => ipcRenderer.invoke('check-ollama'),
selectFileOrDirectory: () => ipcRenderer.invoke('select-file-or-directory'),
startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'),
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
getBinaryPath: (binaryName) => ipcRenderer.invoke('get-binary-path', binaryName),
on: (channel, callback) => ipcRenderer.on(channel, callback),
off: (channel, callback) => ipcRenderer.off(channel, callback),
send: (key) => ipcRenderer.send(key)
});

View File

@@ -10,7 +10,6 @@ type ElectronAPI = {
createChatWindow: (query?: string, dir?: string, version?: string) => void; createChatWindow: (query?: string, dir?: string, version?: string) => void;
logInfo: (txt: string) => void; logInfo: (txt: string) => void;
showNotification: (data: any) => void; showNotification: (data: any) => void;
createWingToWingWindow: (query: string) => void;
openInChrome: (url: string) => void; openInChrome: (url: string) => void;
fetchMetadata: (url: string) => Promise<any>; fetchMetadata: (url: string) => Promise<any>;
reloadApp: () => void; reloadApp: () => void;
@@ -42,7 +41,6 @@ const electronAPI: ElectronAPI = {
ipcRenderer.send('create-chat-window', query, dir, version), ipcRenderer.send('create-chat-window', query, dir, version),
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt), logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
showNotification: (data: any) => ipcRenderer.send('notify', data), showNotification: (data: any) => ipcRenderer.send('notify', data),
createWingToWingWindow: (query: string) => ipcRenderer.send('create-wing-to-wing-window', query),
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url), openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url), fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url),
reloadApp: () => ipcRenderer.send('reload-app'), reloadApp: () => ipcRenderer.send('reload-app'),

View File

@@ -1,33 +0,0 @@
/**
* This file will automatically be loaded by vite and run in the "renderer" context.
* To learn more about the differences between the "main" and the "renderer" context in
* Electron, visit:
*
* https://electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes
*
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
* in a renderer process, please be aware of potential security implications. You can read
* more about security risks here:
*
* https://electronjs.org/docs/tutorial/security
*
* To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
* flag:
*
* ```
* // Create the browser window.
* mainWindow = new BrowserWindow({
* width: 800,
* height: 600,
* webPreferences: {
* nodeIntegration: true
* }
* });
* ```
*/
import "/styles/main.css";
console.log(
'👋 This message is being logged by "renderer.js", included via Vite'
);

View File

@@ -1,77 +1,21 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import { ModelProvider } from './components/settings/models/ModelContext';
import { ErrorBoundary } from './components/ErrorBoundary';
import { ActiveKeysProvider } from './components/settings/api_keys/ActiveKeysContext';
import { patchConsoleLogging } from './utils';
// Error Boundary Component patchConsoleLogging();
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(_: Error) {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Send error to main process
window.electron.logInfo(`[ERROR] ${error.toString()}\n${errorInfo.componentStack}`);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Set up console interceptors
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info,
};
// Intercept console methods
console.log = (...args) => {
window.electron.logInfo(`[LOG] ${args.join(' ')}`);
originalConsole.log(...args);
};
console.error = (...args) => {
window.electron.logInfo(`[ERROR] ${args.join(' ')}`);
originalConsole.error(...args);
};
console.warn = (...args) => {
window.electron.logInfo(`[WARN] ${args.join(' ')}`);
originalConsole.warn(...args);
};
console.info = (...args) => {
window.electron.logInfo(`[INFO] ${args.join(' ')}`);
originalConsole.info(...args);
};
// Capture unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
window.electron.logInfo(`[UNHANDLED REJECTION] ${event.reason}`);
});
// Capture global errors
window.addEventListener('error', (event) => {
window.electron.logInfo(
`[GLOBAL ERROR] ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`
);
});
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<ModelProvider>
<ActiveKeysProvider>
<ErrorBoundary> <ErrorBoundary>
<App /> <App />
</ErrorBoundary> </ErrorBoundary>
</ActiveKeysProvider>
</ModelProvider>
</React.StrictMode> </React.StrictMode>
); );

View File

@@ -11,3 +11,33 @@ export function snakeToTitleCase(snake: string): string {
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' '); .join(' ');
} }
export function patchConsoleLogging() {
// Intercept console methods
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info,
};
console.log = (...args: any[]) => {
window.electron.logInfo(`[LOG] ${args.join(' ')}`);
originalConsole.log(...args);
};
console.error = (...args: any[]) => {
window.electron.logInfo(`[ERROR] ${args.join(' ')}`);
originalConsole.error(...args);
};
console.warn = (...args: any[]) => {
window.electron.logInfo(`[WARN] ${args.join(' ')}`);
originalConsole.warn(...args);
};
console.info = (...args: any[]) => {
window.electron.logInfo(`[INFO] ${args.join(' ')}`);
originalConsole.info(...args);
};
}

View File

@@ -1,13 +0,0 @@
{
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Hello! ping www.apple.com 10 times."
}
]
}
]
}

View File

@@ -1,7 +0,0 @@
# run cargo run -p goose-server: https://github.com/block/goose/pull/237
curl -N http://localhost:3000/reply \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "x-protocol: data" \
-d @test.json