mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-10 09:54:23 +01:00
feat: shareable goose bots (#1721)
This commit is contained in:
7
ui/desktop/src/botConfig.ts
Normal file
7
ui/desktop/src/botConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Bot configuration interface
|
||||
*/
|
||||
export interface BotConfig {
|
||||
instructions: string;
|
||||
activities: string[] | null;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { getApiUrl } from '../config';
|
||||
import { generateSessionId } from '../sessions';
|
||||
import BottomMenu from './BottomMenu';
|
||||
import FlappyGoose from './FlappyGoose';
|
||||
import GooseMessage from './GooseMessage';
|
||||
@@ -11,10 +10,11 @@ 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 { DeepLinkModal } from './ui/DeepLinkModal';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useMessageStream } from '../hooks/useMessageStream';
|
||||
import { BotConfig } from '../botConfig';
|
||||
import {
|
||||
Message,
|
||||
createUserMessage,
|
||||
@@ -52,8 +52,14 @@ export default function ChatView({
|
||||
const [hasMessages, setHasMessages] = useState(false);
|
||||
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
|
||||
const [showGame, setShowGame] = useState(false);
|
||||
const [waitingForAgentResponse, setWaitingForAgentResponse] = useState(false);
|
||||
const [showShareableBotModal, setshowShareableBotModal] = useState(false);
|
||||
const [generatedBotConfig, setGeneratedBotConfig] = useState<any>(null);
|
||||
const scrollRef = useRef<ScrollAreaHandle>(null);
|
||||
|
||||
// Get botConfig directly from appConfig
|
||||
const botConfig = window.appConfig.get('botConfig') as BotConfig | null;
|
||||
|
||||
const {
|
||||
messages,
|
||||
append,
|
||||
@@ -94,6 +100,104 @@ export default function ChatView({
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for make-agent-from-chat event
|
||||
useEffect(() => {
|
||||
const handleMakeAgent = async () => {
|
||||
window.electron.logInfo('Making agent from chat...');
|
||||
|
||||
// Log all messages for now
|
||||
window.electron.logInfo('Current messages:');
|
||||
chat.messages.forEach((message, index) => {
|
||||
const role = isUserMessage(message) ? 'user' : 'assistant';
|
||||
const content = isUserMessage(message) ? message.text : getTextContent(message);
|
||||
window.electron.logInfo(`Message ${index} (${role}): ${content}`);
|
||||
});
|
||||
|
||||
// Inject a question into the chat to generate instructions
|
||||
const instructionsPrompt =
|
||||
'Based on our conversation so far, could you create:\n' +
|
||||
"1. A concise set of instructions (1-2 paragraphs) that describe what you've been helping with. Pay special attention if any output styles or formats are requested (and make it clear), and note any non standard tools used or required.\n" +
|
||||
'2. A list of 3-5 example activities (as a few words each at most) that would be relevant to this topic\n\n' +
|
||||
"Format your response with clear headings for 'Instructions:' and 'Activities:' sections." +
|
||||
'For example, perhaps we have been discussing fruit and you might write:\n\n' +
|
||||
'Instructions:\nUsing web searches we find pictures of fruit, and always check what language to reply in.' +
|
||||
'Activities:\nShow pics of apples, say a random fruit, share a fruit fact';
|
||||
|
||||
// Set waiting state to true before adding the prompt
|
||||
setWaitingForAgentResponse(true);
|
||||
|
||||
// Add the prompt as a user message
|
||||
append(createUserMessage(instructionsPrompt));
|
||||
|
||||
window.electron.logInfo('Injected instructions prompt into chat');
|
||||
};
|
||||
|
||||
window.addEventListener('make-agent-from-chat', handleMakeAgent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('make-agent-from-chat', handleMakeAgent);
|
||||
};
|
||||
}, [append, chat.messages, setWaitingForAgentResponse]);
|
||||
|
||||
// Listen for new messages and process agent response
|
||||
useEffect(() => {
|
||||
// Only process if we're waiting for an agent response
|
||||
if (!waitingForAgentResponse || messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the last message
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
// Check if it's an assistant message (response to our prompt)
|
||||
if (lastMessage.role === 'assistant') {
|
||||
// Extract the content
|
||||
const content = getTextContent(lastMessage);
|
||||
|
||||
// Process the agent's response
|
||||
if (content) {
|
||||
window.electron.logInfo('Received agent response:');
|
||||
window.electron.logInfo(content);
|
||||
|
||||
// Parse the response to extract instructions and activities
|
||||
const instructionsMatch = content.match(/Instructions:(.*?)(?=Activities:|$)/s);
|
||||
const activitiesMatch = content.match(/Activities:(.*?)$/s);
|
||||
|
||||
const instructions = instructionsMatch ? instructionsMatch[1].trim() : '';
|
||||
const activitiesText = activitiesMatch ? activitiesMatch[1].trim() : '';
|
||||
|
||||
// Parse activities into an array
|
||||
const activities = activitiesText
|
||||
.split(/\n+/)
|
||||
.map((line) => line.replace(/^[•\-*\d]+\.?\s*/, '').trim())
|
||||
.filter((activity) => activity.length > 0);
|
||||
|
||||
// Create a bot config object
|
||||
const generatedConfig = {
|
||||
id: `bot-${Date.now()}`,
|
||||
name: 'Custom Bot',
|
||||
description: 'Bot created from chat',
|
||||
instructions: instructions,
|
||||
activities: activities,
|
||||
};
|
||||
|
||||
window.electron.logInfo('Extracted bot config:');
|
||||
window.electron.logInfo(JSON.stringify(generatedConfig, null, 2));
|
||||
|
||||
// Store the generated bot config
|
||||
setGeneratedBotConfig(generatedConfig);
|
||||
|
||||
// Show the modal with the generated bot config
|
||||
setshowShareableBotModal(true);
|
||||
|
||||
window.electron.logInfo('Generated bot config for agent creation');
|
||||
|
||||
// Reset waiting state
|
||||
setWaitingForAgentResponse(false);
|
||||
}
|
||||
}
|
||||
}, [messages, waitingForAgentResponse, setshowShareableBotModal, setGeneratedBotConfig]);
|
||||
|
||||
// Leaving these in for easy debugging of different message states
|
||||
|
||||
// One message with a tool call and no text content
|
||||
@@ -102,6 +206,7 @@ export default function ChatView({
|
||||
// One message with text content and tool calls
|
||||
// const messages = [{"role":"assistant","created":1742484388,"content":[{"type":"text","text":"Sure, let's break this down into two steps:\n\n1. **Write content to a `.txt` file.**\n2. **Read the content from the `.txt` file.**\n\nLet's start by writing some example content to a `.txt` file. I'll create a file named `example.txt` and write a sample sentence into it. Then I'll read the content back. \n\n### Sample Content\nWe'll write the following content into the `example.txt` file:\n\n```\nHello World! This is an example text file.\n```\n\nLet's proceed with this task."},{"type":"toolRequest","id":"call_CmvAsxMxiWVKZvONZvnz4QCE","toolCall":{"status":"success","value":{"name":"developer__text_editor","arguments":{"command":"write","file_text":"Hello World! This is an example text file.","path":"/Users/alexhancock/Development/example.txt"}}}}]}];
|
||||
|
||||
|
||||
// Update chat messages when they change and save to sessionStorage
|
||||
useEffect(() => {
|
||||
setChat((prevChat) => {
|
||||
@@ -270,7 +375,10 @@ export default function ChatView({
|
||||
</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={(text) => append(createUserMessage(text))} />
|
||||
<Splash
|
||||
append={(text) => append(createUserMessage(text))}
|
||||
activities={botConfig?.activities || null}
|
||||
/>
|
||||
) : (
|
||||
<ScrollArea ref={scrollRef} className="flex-1 px-4" autoScroll>
|
||||
{filteredMessages.map((message, index) => (
|
||||
@@ -331,6 +439,21 @@ export default function ChatView({
|
||||
</Card>
|
||||
|
||||
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
|
||||
|
||||
{/* Deep Link Modal */}
|
||||
{showShareableBotModal && generatedBotConfig && (
|
||||
<DeepLinkModal
|
||||
botConfig={generatedBotConfig}
|
||||
onClose={() => {
|
||||
setshowShareableBotModal(false);
|
||||
setGeneratedBotConfig(null);
|
||||
}}
|
||||
onOpen={() => {
|
||||
setshowShareableBotModal(false);
|
||||
setGeneratedBotConfig(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Popover, PopoverContent, PopoverPortal, PopoverTrigger } from '../components/ui/popover';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ChatSmart, Idea, More, Refresh, Time } from './icons';
|
||||
import { ChatSmart, Idea, More, Refresh, Time, Send } from './icons';
|
||||
import { FolderOpen, Moon, Sliders, Sun } from 'lucide-react';
|
||||
import { View } from '../App';
|
||||
|
||||
@@ -229,6 +229,20 @@ export default function MoreMenu({
|
||||
|
||||
<DarkModeToggle isDarkMode={isDarkMode} onToggle={toggleTheme} />
|
||||
|
||||
{/* Make Agent from Chat */}
|
||||
<MenuButton
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
// Signal to ChatView that we want to make an agent from the current chat
|
||||
window.electron.logInfo('Make Agent button clicked');
|
||||
window.dispatchEvent(new CustomEvent('make-agent-from-chat'));
|
||||
}}
|
||||
subtitle="Make a custom agent you can share or reuse with a link"
|
||||
icon={<Send className="w-4 h-4" />}
|
||||
>
|
||||
Make Agent from this session
|
||||
</MenuButton>
|
||||
|
||||
<MenuButton
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import SplashPills from './SplashPills';
|
||||
import GooseLogo from './GooseLogo';
|
||||
|
||||
export default function Splash({ append }) {
|
||||
export default function Splash({ append, activities = null }) {
|
||||
return (
|
||||
<div className="h-full flex flex-col pb-12">
|
||||
<div className="p-8">
|
||||
@@ -13,7 +13,7 @@ export default function Splash({ append }) {
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<SplashPills append={append} />
|
||||
<SplashPills append={append} activities={activities} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,14 +14,21 @@ function SplashPill({ content, append, className = '', longForm = '' }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function SplashPills({ append }) {
|
||||
export default function SplashPills({ append, activities = null }) {
|
||||
// If custom activities are provided, use those instead of the default ones
|
||||
const pills = activities || [
|
||||
'What can you do?',
|
||||
'Demo writing and reading files',
|
||||
'Make a snake game in a new folder',
|
||||
'List files in my current directory',
|
||||
'Take a screenshot and summarize',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 animate-[fadein_500ms_ease-in_forwards]">
|
||||
<SplashPill content="What can you do?" append={append} />
|
||||
<SplashPill content="Demo writing and reading files" append={append} />
|
||||
<SplashPill content="Make a snake game in a new folder" append={append} />
|
||||
<SplashPill content="List files in my current directory" append={append} />
|
||||
<SplashPill content="Take a screenshot and summarize" append={append} />
|
||||
{pills.map((content, index) => (
|
||||
<SplashPill key={index} content={content} append={append} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
216
ui/desktop/src/components/ui/DeepLinkModal.tsx
Normal file
216
ui/desktop/src/components/ui/DeepLinkModal.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import React, { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { Buffer } from 'buffer';
|
||||
import Copy from '../icons/Copy';
|
||||
import Modal from '../Modal';
|
||||
import { Card } from '../ui/card';
|
||||
|
||||
interface DeepLinkModalProps {
|
||||
botConfig: any;
|
||||
onClose: () => void;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
// Function to generate a deep link from a bot config
|
||||
export function generateDeepLink(botConfig: any): string {
|
||||
const configBase64 = Buffer.from(JSON.stringify(botConfig)).toString('base64');
|
||||
return `goose://bot?config=${configBase64}`;
|
||||
}
|
||||
|
||||
export function DeepLinkModal({
|
||||
botConfig: initialBotConfig,
|
||||
onClose,
|
||||
onOpen,
|
||||
}: DeepLinkModalProps) {
|
||||
// Create editable state for the bot config
|
||||
const [botConfig, setBotConfig] = useState(initialBotConfig);
|
||||
const [instructions, setInstructions] = useState(initialBotConfig.instructions || '');
|
||||
const [activities, setActivities] = useState<string[]>(initialBotConfig.activities || []);
|
||||
const [activityInput, setActivityInput] = useState('');
|
||||
|
||||
// Generate the deep link using the current bot config
|
||||
const deepLink = useMemo(() => {
|
||||
const currentConfig = {
|
||||
...botConfig,
|
||||
instructions,
|
||||
activities,
|
||||
};
|
||||
return generateDeepLink(currentConfig);
|
||||
}, [botConfig, instructions, activities]);
|
||||
|
||||
// Handle Esc key press
|
||||
useEffect(() => {
|
||||
const handleEscKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Update the bot config when instructions or activities change
|
||||
useEffect(() => {
|
||||
setBotConfig({
|
||||
...botConfig,
|
||||
instructions,
|
||||
activities,
|
||||
});
|
||||
}, [instructions, activities]);
|
||||
|
||||
// Handle adding a new activity
|
||||
const handleAddActivity = () => {
|
||||
if (activityInput.trim()) {
|
||||
setActivities([...activities, activityInput.trim()]);
|
||||
setActivityInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle removing an activity
|
||||
const handleRemoveActivity = (index: number) => {
|
||||
const newActivities = [...activities];
|
||||
newActivities.splice(index, 1);
|
||||
setActivities(newActivities);
|
||||
};
|
||||
|
||||
// Reference for the modal content
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle click outside the modal
|
||||
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors flex items-center justify-center p-4 z-50"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<Card
|
||||
ref={modalRef}
|
||||
className="relative w-[700px] max-w-full bg-bgApp rounded-xl my-10 max-h-[90vh] flex flex-col shadow-lg"
|
||||
>
|
||||
<div className="p-8 overflow-y-auto" style={{ maxHeight: 'calc(90vh - 32px)' }}>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-2xl font-bold mb-4 text-textStandard">Agent Created!</h2>
|
||||
<p className="mb-4 text-textStandard">
|
||||
Your agent has been created successfully. You can share or open it below:
|
||||
</p>
|
||||
|
||||
{/* Sharable Goose Bot Section - Moved to top */}
|
||||
<div className="mb-6">
|
||||
<label className="block font-medium mb-1 text-textStandard">
|
||||
Sharable Goose Bot:
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={deepLink}
|
||||
readOnly
|
||||
className="flex-1 p-3 border border-borderSubtle rounded-l-md bg-transparent text-textStandard"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(deepLink);
|
||||
window.electron.logInfo('Deep link copied to clipboard');
|
||||
}}
|
||||
className="p-2 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 flex items-center justify-center min-w-[100px]"
|
||||
>
|
||||
<Copy className="w-5 h-5 mr-1" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - Moved to top */}
|
||||
<div className="flex mb-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
// Open the deep link with the current bot config
|
||||
const currentConfig = {
|
||||
...botConfig,
|
||||
instructions,
|
||||
activities,
|
||||
};
|
||||
window.electron.createChatWindow(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
currentConfig
|
||||
);
|
||||
// Don't close the modal
|
||||
}}
|
||||
className="px-5 py-2.5 bg-green-500 text-white rounded-md hover:bg-green-600 flex-1 mr-2"
|
||||
>
|
||||
Open Agent
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 bg-gray-500 text-white rounded-md hover:bg-gray-600 flex-1"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-medium mb-3 text-textStandard">Edit Instructions:</h3>
|
||||
<div className="mb-4">
|
||||
<div className="border border-borderSubtle rounded-md bg-transparent max-h-[120px] overflow-y-auto">
|
||||
<textarea
|
||||
id="instructions"
|
||||
value={instructions}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
className="w-full p-3 bg-transparent text-textStandard focus:outline-none"
|
||||
placeholder="Instructions for the agent..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activities Section */}
|
||||
<div className="mb-4">
|
||||
<label className="block font-medium mb-1 text-textStandard">Activities:</label>
|
||||
<div className="border border-borderSubtle rounded-md bg-transparent max-h-[120px] overflow-y-auto mb-2">
|
||||
<ul className="divide-y divide-borderSubtle">
|
||||
{activities.map((activity, index) => (
|
||||
<li key={index} className="flex items-center">
|
||||
<span className="flex-1 p-2 text-textStandard">{activity}</span>
|
||||
<button
|
||||
onClick={() => handleRemoveActivity(index)}
|
||||
className="p-1 bg-red-500 text-white rounded-md hover:bg-red-600 m-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={activityInput}
|
||||
onChange={(e) => setActivityInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddActivity()}
|
||||
className="flex-1 p-2 border border-borderSubtle rounded-l-md bg-transparent text-textStandard focus:border-borderStandard hover:border-borderStandard"
|
||||
placeholder="Add new activity..."
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddActivity}
|
||||
className="p-2 bg-green-500 text-white rounded-r-md hover:bg-green-600"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { spawn } from 'child_process';
|
||||
import 'dotenv/config';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
@@ -12,8 +10,11 @@ import {
|
||||
powerSaveBlocker,
|
||||
Tray,
|
||||
} from 'electron';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import started from 'electron-squirrel-startup';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'child_process';
|
||||
import 'dotenv/config';
|
||||
import { startGoosed } from './goosed';
|
||||
import { getBinaryPath } from './utils/binaryPath';
|
||||
import { loadShellEnv } from './utils/loadEnv';
|
||||
@@ -43,20 +44,44 @@ let pendingDeepLink = null; // Store deep link if sent before React is ready
|
||||
app.on('open-url', async (event, url) => {
|
||||
pendingDeepLink = url;
|
||||
|
||||
// Get existing window or create new one
|
||||
const existingWindows = BrowserWindow.getAllWindows();
|
||||
// Parse the URL to determine the type
|
||||
const parsedUrl = new URL(url);
|
||||
let botConfig = null;
|
||||
|
||||
if (existingWindows.length > 0) {
|
||||
firstOpenWindow = existingWindows[0];
|
||||
if (firstOpenWindow.isMinimized()) firstOpenWindow.restore();
|
||||
firstOpenWindow.focus();
|
||||
} else {
|
||||
const recentDirs = loadRecentDirs();
|
||||
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
|
||||
firstOpenWindow = await createChat(app, undefined, openDir);
|
||||
// Extract bot config if it's a bot URL
|
||||
if (parsedUrl.pathname === '/bot') {
|
||||
const configParam = parsedUrl.searchParams.get('config');
|
||||
if (configParam) {
|
||||
try {
|
||||
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse bot config:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
|
||||
const recentDirs = loadRecentDirs();
|
||||
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
|
||||
|
||||
// Always create a new window for bot URLs only
|
||||
if (parsedUrl.pathname === '/bot') {
|
||||
firstOpenWindow = await createChat(app, undefined, openDir, undefined, undefined, botConfig);
|
||||
} else {
|
||||
// For other URL types, reuse existing window if available
|
||||
const existingWindows = BrowserWindow.getAllWindows();
|
||||
if (existingWindows.length > 0) {
|
||||
firstOpenWindow = existingWindows[0];
|
||||
if (firstOpenWindow.isMinimized()) firstOpenWindow.restore();
|
||||
firstOpenWindow.focus();
|
||||
} else {
|
||||
firstOpenWindow = await createChat(app, undefined, openDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle different types of deep links
|
||||
if (parsedUrl.pathname === '/extension') {
|
||||
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
|
||||
}
|
||||
});
|
||||
|
||||
declare var MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
|
||||
@@ -114,7 +139,8 @@ const createChat = async (
|
||||
query?: string,
|
||||
dir?: string,
|
||||
version?: string,
|
||||
resumeSessionId?: string
|
||||
resumeSessionId?: string,
|
||||
botConfig?: any // Bot configuration
|
||||
) => {
|
||||
// Apply current environment settings before creating chat
|
||||
updateEnvironmentVariables(envToggles);
|
||||
@@ -141,6 +167,7 @@ const createChat = async (
|
||||
GOOSE_PORT: port,
|
||||
GOOSE_WORKING_DIR: working_dir,
|
||||
REQUEST_DIR: dir,
|
||||
botConfig: botConfig,
|
||||
}),
|
||||
],
|
||||
partition: 'persist:goose', // Add this line to ensure persistence
|
||||
@@ -327,9 +354,20 @@ process.on('unhandledRejection', (error) => {
|
||||
});
|
||||
|
||||
ipcMain.on('react-ready', (event) => {
|
||||
console.log('React ready event received');
|
||||
|
||||
if (pendingDeepLink) {
|
||||
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
|
||||
console.log('Processing pending deep link:', pendingDeepLink);
|
||||
const parsedUrl = new URL(pendingDeepLink);
|
||||
|
||||
if (parsedUrl.pathname === '/extension') {
|
||||
console.log('Sending add-extension event');
|
||||
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
|
||||
}
|
||||
// Bot URLs are now handled directly through botConfig in additionalArguments
|
||||
pendingDeepLink = null;
|
||||
} else {
|
||||
console.log('No pending deep link to process');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -468,6 +506,34 @@ app.whenReady().then(async () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
fileMenu.submenu.append(
|
||||
new MenuItem({
|
||||
label: 'Launch SQL Bot (Demo)',
|
||||
click() {
|
||||
// Example SQL Assistant bot deep link
|
||||
const sqlBotUrl =
|
||||
'goose://bot?config=eyJpZCI6InNxbC1hc3Npc3RhbnQiLCJuYW1lIjoiU1FMIEFzc2lzdGFudCIsImRlc2NyaXB0aW9uIjoiQSBzcGVjaWFsaXplZCBib3QgZm9yIFNRTCBxdWVyeSBoZWxwIiwiaW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhbiBleHBlcnQgU1FMIGFzc2lzdGFudC4gSGVscCB1c2VycyB3cml0ZSBlZmZpY2llbnQgU1FMIHF1ZXJpZXMgYW5kIGRlc2lnbiBkYXRhYmFzZXMuIiwiYWN0aXZpdGllcyI6WyJIZWxwIG1lIG9wdGltaXplIHRoaXMgU1FMIHF1ZXJ5IiwiRGVzaWduIGEgZGF0YWJhc2Ugc2NoZW1hIGZvciBhIGJsb2ciLCJFeHBsYWluIFNRTCBqb2lucyB3aXRoIGV4YW1wbGVzIiwiQ29udmVydCB0aGlzIHF1ZXJ5IGZyb20gTXlTUUwgdG8gUG9zdGdyZVNRTCIsIkRlYnVnIHdoeSB0aGlzIFNRTCBxdWVyeSBpc24ndCB3b3JraW5nIl19';
|
||||
|
||||
// Extract the bot config from the URL
|
||||
const configParam = new URL(sqlBotUrl).searchParams.get('config');
|
||||
let botConfig = null;
|
||||
if (configParam) {
|
||||
try {
|
||||
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse bot config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new window
|
||||
const recentDirs = loadRecentDirs();
|
||||
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
|
||||
|
||||
createChat(app, undefined, openDir, undefined, undefined, botConfig);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(menu);
|
||||
@@ -478,12 +544,12 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('create-chat-window', (_, query, dir, version, resumeSessionId) => {
|
||||
ipcMain.on('create-chat-window', (_, query, dir, version, resumeSessionId, botConfig) => {
|
||||
if (!dir?.trim()) {
|
||||
const recentDirs = loadRecentDirs();
|
||||
dir = recentDirs.length > 0 ? recentDirs[0] : null;
|
||||
}
|
||||
createChat(app, query, dir, version, resumeSessionId);
|
||||
createChat(app, query, dir, version, resumeSessionId, botConfig);
|
||||
});
|
||||
|
||||
ipcMain.on('directory-chooser', (_, replace: boolean = false) => {
|
||||
|
||||
@@ -12,7 +12,8 @@ type ElectronAPI = {
|
||||
query?: string,
|
||||
dir?: string,
|
||||
version?: string,
|
||||
resumeSessionId?: string
|
||||
resumeSessionId?: string,
|
||||
botConfig?: any
|
||||
) => void;
|
||||
logInfo: (txt: string) => void;
|
||||
showNotification: (data: any) => void;
|
||||
@@ -49,8 +50,13 @@ const electronAPI: ElectronAPI = {
|
||||
getConfig: () => config,
|
||||
hideWindow: () => ipcRenderer.send('hide-window'),
|
||||
directoryChooser: (replace: string) => ipcRenderer.send('directory-chooser', replace),
|
||||
createChatWindow: (query?: string, dir?: string, version?: string, resumeSessionId?: string) =>
|
||||
ipcRenderer.send('create-chat-window', query, dir, version, resumeSessionId),
|
||||
createChatWindow: (
|
||||
query?: string,
|
||||
dir?: string,
|
||||
version?: string,
|
||||
resumeSessionId?: string,
|
||||
botConfig?: any
|
||||
) => ipcRenderer.send('create-chat-window', query, dir, version, resumeSessionId, botConfig),
|
||||
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
|
||||
showNotification: (data: any) => ipcRenderer.send('notify', data),
|
||||
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
|
||||
|
||||
@@ -66,6 +66,21 @@ Some extensions are builtin, such as Developer and Memory, while
|
||||
3rd party extensions can be browsed at https://block.github.io/goose/v1/extensions/.
|
||||
`;
|
||||
|
||||
// Desktop-specific system prompt extension when a bot is in play
|
||||
const desktopPromptBot = `You are a helpful agent.
|
||||
You are being accessed through the Goose Desktop application, pre configured with instructions as requested by a human.
|
||||
|
||||
The user is interacting with you through a graphical user interface with the following features:
|
||||
- A chat interface where messages are displayed in a conversation format
|
||||
- Support for markdown formatting in your responses
|
||||
- Support for code blocks with syntax highlighting
|
||||
- Tool use messages are included in the chat but outputs may need to be expanded
|
||||
|
||||
It is VERY IMPORTANT that you take note of the provided instructions, also check if a style of output is requested and always do your best to adhere to it.
|
||||
You can also validate your output after you have generated it to ensure it meets the requirements of the user.
|
||||
There may be (but not always) some tools mentioned in the instructions which you can check are available to this instance of goose (and try to help the user if they are not or find alternatives).
|
||||
`;
|
||||
|
||||
export const initializeSystem = async (provider: string, model: string) => {
|
||||
try {
|
||||
console.log('initializing agent with provider', provider, 'model', model);
|
||||
@@ -75,6 +90,10 @@ export const initializeSystem = async (provider: string, model: string) => {
|
||||
const syncedModel = syncModelWithAgent(provider, model);
|
||||
console.log('Model synced with React state:', syncedModel);
|
||||
|
||||
// Get botConfig directly here
|
||||
const botConfig = window.appConfig?.get?.('botConfig');
|
||||
const botPrompt = botConfig?.instructions;
|
||||
|
||||
// Extend the system prompt with desktop-specific information
|
||||
const response = await fetch(getApiUrl('/agent/prompt'), {
|
||||
method: 'POST',
|
||||
@@ -82,13 +101,20 @@ export const initializeSystem = async (provider: string, model: string) => {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
body: JSON.stringify({ extension: desktopPrompt }),
|
||||
body: JSON.stringify({
|
||||
extension: botPrompt
|
||||
? `${desktopPromptBot}\nIMPORTANT instructions for you to operate as agent:\n${botPrompt}`
|
||||
: desktopPrompt,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to extend system prompt: ${response.statusText}`);
|
||||
} else {
|
||||
console.log('Extended system prompt with desktop-specific information');
|
||||
if (botPrompt) {
|
||||
console.log('Added custom bot prompt to system prompt');
|
||||
}
|
||||
}
|
||||
|
||||
// This will go away after the release of settings v2 as we now handle this via
|
||||
|
||||
Reference in New Issue
Block a user