mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Add goose versions to the UI (#526)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
||||
@@ -17,4 +17,4 @@ export default function BottomMenu({hasMessages}) {
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {Popover, PopoverContent, PopoverTrigger} from './ui/popover';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
PopoverPortal,
|
||||
} from "@radix-ui/react-popover";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FaMoon, FaSun } from 'react-icons/fa';
|
||||
import VertDots from './ui/VertDots';
|
||||
import {FaSun, FaMoon} from 'react-icons/fa';
|
||||
|
||||
interface VersionInfo {
|
||||
current_version: string;
|
||||
available_versions: string[];
|
||||
}
|
||||
|
||||
export default function MoreMenu() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [versions, setVersions] = useState<VersionInfo | null>(null);
|
||||
const [showVersions, setShowVersions] = useState(false);
|
||||
|
||||
const [useSystemTheme, setUseSystemTheme] = useState(() =>
|
||||
localStorage.getItem('use_system_theme') === 'true'
|
||||
@@ -20,6 +31,27 @@ export default function MoreMenu() {
|
||||
return savedTheme ? savedTheme === 'dark' : systemPrefersDark;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch available versions when the menu opens
|
||||
const fetchVersions = async () => {
|
||||
try {
|
||||
const port = window.appConfig.get("GOOSE_SERVER__PORT");
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/agent/versions`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setVersions(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch versions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchVersions();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
@@ -76,6 +108,13 @@ export default function MoreMenu() {
|
||||
// If disabling system theme, keep current theme state but don't update localStorage yet
|
||||
};
|
||||
|
||||
const handleVersionSelect = (version: string) => {
|
||||
setOpen(false);
|
||||
setShowVersions(false);
|
||||
// Create a new chat window with the selected version
|
||||
window.electron.createChatWindow(undefined, undefined, version);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -84,52 +123,87 @@ export default function MoreMenu() {
|
||||
<VertDots size={18}/>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 rounded-md">
|
||||
<div className="flex flex-col bg-black text-white dark:bg-gray-800 rounded-md">
|
||||
<div className="flex items-center justify-between p-2">
|
||||
<span className="text-sm">Use System Theme</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useSystemTheme}
|
||||
onChange={toggleUseSystemTheme}
|
||||
/>
|
||||
</div>
|
||||
{!useSystemTheme && (<div className="flex items-center justify-between p-2">
|
||||
<span className="text-sm">{isDarkMode ? 'Dark Mode' : 'Light Mode'}</span>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
className="z-[200] w-48 rounded-md bg-black text-white dark:bg-gray-800 shadow-lg"
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div className="flex flex-col rounded-md">
|
||||
<div className="flex items-center justify-between p-2">
|
||||
<span className="text-sm">Use System Theme</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useSystemTheme}
|
||||
onChange={toggleUseSystemTheme}
|
||||
/>
|
||||
</div>
|
||||
{!useSystemTheme && (<div className="flex items-center justify-between p-2">
|
||||
<span className="text-sm">{isDarkMode ? 'Dark Mode' : 'Light Mode'}</span>
|
||||
<button
|
||||
className={`relative inline-flex items-center h-6 rounded-full w-11 focus:outline-none border-2 ${isDarkMode
|
||||
? 'bg-gray-600 border-gray-600'
|
||||
: 'bg-yellow-300 border-yellow-300'}`}
|
||||
onClick={() => toggleTheme()}>
|
||||
<span
|
||||
className={`inline-block w-4 h-4 transform bg-white rounded-full transition-transform ${isDarkMode
|
||||
? 'translate-x-6' : 'translate-x-1'}`}
|
||||
>
|
||||
{isDarkMode ? <FaMoon className="text-gray-200"/> : <FaSun
|
||||
className="text-yellow-500"/>}
|
||||
</span>
|
||||
</button>
|
||||
</div>)}
|
||||
|
||||
{/* Versions Menu */}
|
||||
{versions && versions.available_versions.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowVersions(!showVersions)}
|
||||
className="w-full text-left px-2 py-1.5 text-sm hover:bg-gray-700 flex justify-between items-center"
|
||||
>
|
||||
<span>Versions</span>
|
||||
<span className="text-xs">{showVersions ? '▼' : '▶'}</span>
|
||||
</button>
|
||||
{showVersions && (
|
||||
<div className="pl-2 bg-gray-900">
|
||||
{versions.available_versions.map((version) => (
|
||||
<button
|
||||
key={version}
|
||||
onClick={() => handleVersionSelect(version)}
|
||||
className={`w-full text-left px-2 py-1.5 text-sm hover:bg-gray-700 ${
|
||||
version === versions.current_version ? 'text-green-400' : ''
|
||||
}`}
|
||||
>
|
||||
{version} {version === versions.current_version && '(current)'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className={`relative inline-flex items-center h-6 rounded-full w-11 focus:outline-none border-2 ${isDarkMode
|
||||
? 'bg-gray-600 border-gray-600'
|
||||
: 'bg-yellow-300 border-yellow-300'}`}
|
||||
onClick={() => toggleTheme()}>
|
||||
<span
|
||||
className={`inline-block w-4 h-4 transform bg-white rounded-full transition-transform ${isDarkMode
|
||||
? 'translate-x-6' : 'translate-x-1'}`}
|
||||
>
|
||||
{isDarkMode ? <FaMoon className="text-gray-200"/> : <FaSun
|
||||
className="text-yellow-500"/>}
|
||||
</span>
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
window.electron.directoryChooser();
|
||||
}}
|
||||
className="w-full text-left px-2 py-1.5 text-sm hover:bg-gray-700"
|
||||
>
|
||||
Open Directory (cmd+O)
|
||||
</button>
|
||||
</div>)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
window.electron.directoryChooser();
|
||||
}}
|
||||
className="w-full text-left px-2 py-1.5 text-sm"
|
||||
>
|
||||
Open Directory (cmd+O)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
window.electron.createChatWindow();
|
||||
}}
|
||||
className="w-full text-left px-2 py-1.5 text-sm"
|
||||
>
|
||||
New Session (cmd+N)
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
window.electron.createChatWindow();
|
||||
}}
|
||||
className="w-full text-left px-2 py-1.5 text-sm hover:bg-gray-700"
|
||||
>
|
||||
New Session (cmd+N)
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
+34
-18
@@ -1,19 +1,14 @@
|
||||
import path from 'node:path';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { getBinaryPath } from './utils/binaryPath';
|
||||
import { existsSync } from 'fs';
|
||||
import log from './utils/logger';
|
||||
import os from 'node:os';
|
||||
import { spawn } from 'child_process';
|
||||
import { createServer } from 'net';
|
||||
import { loadZshEnv } from './utils/loadEnv';
|
||||
|
||||
import os from 'node:os';
|
||||
import { getBinaryPath } from './utils/binaryPath';
|
||||
import log from './utils/logger';
|
||||
|
||||
// Find an available port to start goosed on
|
||||
export const findAvailablePort = (): Promise<number> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer();
|
||||
|
||||
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const { port } = server.address() as { port: number };
|
||||
server.close(() => {
|
||||
@@ -24,9 +19,24 @@ export const findAvailablePort = (): Promise<number> => {
|
||||
});
|
||||
};
|
||||
|
||||
// Function to fetch agent version from the server
|
||||
const fetchAgentVersion = async (port: number): Promise<string> => {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/agent/versions`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.current_version;
|
||||
} catch (error) {
|
||||
log.error('Failed to fetch agent version:', error);
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
// Goose process manager. Take in the app, port, and directory to start goosed in.
|
||||
// Check if goosed server is ready by polling the status endpoint
|
||||
const checkServerStatus = async (port: number, maxAttempts: number = 30, interval: number = 100): Promise<boolean> => {
|
||||
const checkServerStatus = async (port: number, maxAttempts: number = 60, interval: number = 100): Promise<boolean> => {
|
||||
const statusUrl = `http://127.0.0.1:${port}/status`;
|
||||
log.info(`Checking server status at ${statusUrl}`);
|
||||
|
||||
@@ -48,7 +58,7 @@ const checkServerStatus = async (port: number, maxAttempts: number = 30, interva
|
||||
return false;
|
||||
};
|
||||
|
||||
export const startGoosed = async (app, dir=null): Promise<[number, string]> => {
|
||||
export const startGoosed = async (app, dir=null, env={}): Promise<[number, string, string]> => {
|
||||
// In will use this later to determine if we should start process
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
@@ -61,7 +71,7 @@ export const startGoosed = async (app, dir=null): Promise<[number, string]> => {
|
||||
// Skip starting goosed if configured in dev mode
|
||||
if (isDev && !app.isPackaged && process.env.VITE_START_EMBEDDED_SERVER === 'no') {
|
||||
log.info('Skipping starting goosed in development mode');
|
||||
return [3000, dir];
|
||||
return [3000, dir, 'dev'];
|
||||
}
|
||||
|
||||
// Get the goosed binary path using the shared utility
|
||||
@@ -83,13 +93,16 @@ export const startGoosed = async (app, dir=null): Promise<[number, string]> => {
|
||||
GOOSE_SERVER__PORT: String(port),
|
||||
|
||||
GOOSE_SERVER__SECRET_KEY: process.env.GOOSE_SERVER__SECRET_KEY,
|
||||
|
||||
// Add any additional environment variables passed in
|
||||
...env
|
||||
};
|
||||
|
||||
// Merge parent environment with additional environment variables
|
||||
const env = { ...process.env, ...additionalEnv };
|
||||
const processEnv = { ...process.env, ...additionalEnv };
|
||||
|
||||
// Spawn the goosed process with the user's home directory as cwd
|
||||
const goosedProcess = spawn(goosedPath, ["agent"], { cwd: dir, env: env, stdio: ["ignore", "pipe", "pipe"] });
|
||||
const goosedProcess = spawn(goosedPath, ["agent"], { cwd: dir, env: processEnv, stdio: ["ignore", "pipe", "pipe"] });
|
||||
|
||||
goosedProcess.stdout.on('data', (data) => {
|
||||
log.info(`goosed stdout for port ${port} and dir ${dir}: ${data.toString()}`);
|
||||
@@ -110,6 +123,7 @@ export const startGoosed = async (app, dir=null): Promise<[number, string]> => {
|
||||
|
||||
// Wait for the server to be ready
|
||||
const isReady = await checkServerStatus(port);
|
||||
log.info(`Goosed isReady ${isReady}`);
|
||||
if (!isReady) {
|
||||
log.error(`Goosed server failed to start on port ${port}`);
|
||||
goosedProcess.kill();
|
||||
@@ -123,8 +137,10 @@ export const startGoosed = async (app, dir=null): Promise<[number, string]> => {
|
||||
goosedProcess.kill();
|
||||
});
|
||||
|
||||
// Wait for the server to start and fetch the agent version
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Give the server time to start
|
||||
const agentVersion = await fetchAgentVersion(port);
|
||||
|
||||
log.info(`Goosed server successfully started on port ${port}`);
|
||||
return [port, dir];
|
||||
};
|
||||
|
||||
|
||||
return [port, dir, agentVersion];
|
||||
};
|
||||
+22
-14
@@ -1,14 +1,14 @@
|
||||
import { exec } from 'child_process';
|
||||
import 'dotenv/config';
|
||||
import { loadZshEnv } from './utils/loadEnv';
|
||||
import { getBinaryPath } from './utils/binaryPath';
|
||||
import { app, BrowserWindow, Tray, Menu, globalShortcut, ipcMain, Notification, MenuItem, dialog, powerSaveBlocker } from 'electron';
|
||||
import { app, BrowserWindow, dialog, globalShortcut, ipcMain, Menu, MenuItem, Notification, powerSaveBlocker, Tray } from 'electron';
|
||||
import started from "electron-squirrel-startup";
|
||||
import path from 'node:path';
|
||||
import { startGoosed } from './goosed';
|
||||
import started from "electron-squirrel-startup";
|
||||
import { getBinaryPath } from './utils/binaryPath';
|
||||
import { loadZshEnv } from './utils/loadEnv';
|
||||
import log from './utils/logger';
|
||||
import { exec } from 'child_process';
|
||||
import { addRecentDir, loadRecentDirs } from './utils/recentDirs';
|
||||
import { EnvToggles, loadSettings, saveSettings, updateEnvironmentVariables, createEnvironmentMenu } from './utils/settings';
|
||||
import { createEnvironmentMenu, EnvToggles, loadSettings, saveSettings, updateEnvironmentVariables } from './utils/settings';
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (started) app.quit();
|
||||
@@ -70,6 +70,7 @@ let appConfig = {
|
||||
GOOSE_API_HOST: 'http://127.0.0.1',
|
||||
GOOSE_SERVER__PORT: 0,
|
||||
GOOSE_WORKING_DIR: '',
|
||||
GOOSE_AGENT_VERSION: '',
|
||||
secretKey: generateSecretKey(),
|
||||
};
|
||||
|
||||
@@ -114,12 +115,13 @@ const createLauncher = () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Track windows by ID
|
||||
let windowCounter = 0;
|
||||
const windowMap = new Map<number, BrowserWindow>();
|
||||
|
||||
const createChat = async (app, query?: string, dir?: string) => {
|
||||
const createChat = async (app, query?: string, dir?: string, version?: string) => {
|
||||
const env = version ? { GOOSE_AGENT_VERSION: version } : {};
|
||||
|
||||
// Apply current environment settings before creating chat
|
||||
updateEnvironmentVariables(envToggles);
|
||||
|
||||
@@ -127,11 +129,12 @@ const createChat = async (app, query?: string, dir?: string) => {
|
||||
if (checkApiCredentials()) {
|
||||
return startGoosed(app, dir);
|
||||
} else {
|
||||
return [0, ''];
|
||||
return [0, '', ''];
|
||||
}
|
||||
}
|
||||
|
||||
const [port, working_dir] = await maybeStartGoosed();
|
||||
const [port, working_dir, agentVersion] = await maybeStartGoosed();
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
titleBarStyle: 'hidden',
|
||||
trafficLightPosition: { x: 16, y: 10 },
|
||||
@@ -145,7 +148,13 @@ const createChat = async (app, query?: string, dir?: string) => {
|
||||
icon: path.join(__dirname, '../images/icon'),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
additionalArguments: [JSON.stringify({ ...appConfig, GOOSE_SERVER__PORT: port, GOOSE_WORKING_DIR: working_dir, REQUEST_DIR: dir })],
|
||||
additionalArguments: [JSON.stringify({
|
||||
...appConfig,
|
||||
GOOSE_SERVER__PORT: port,
|
||||
GOOSE_WORKING_DIR: working_dir,
|
||||
GOOSE_AGENT_VERSION: agentVersion,
|
||||
REQUEST_DIR: dir
|
||||
})],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -207,7 +216,6 @@ const createTray = () => {
|
||||
tray.setContextMenu(contextMenu);
|
||||
};
|
||||
|
||||
|
||||
const showWindow = () => {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
|
||||
@@ -373,8 +381,8 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('create-chat-window', (_, query) => {
|
||||
createChat(app, query);
|
||||
ipcMain.on('create-chat-window', (_, query, dir, version) => {
|
||||
createChat(app, query, dir, version);
|
||||
});
|
||||
|
||||
ipcMain.on('directory-chooser', (_, replace: boolean = false) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
getConfig: () => config,
|
||||
hideWindow: () => ipcRenderer.send('hide-window'),
|
||||
directoryChooser: (replace) => ipcRenderer.send('directory-chooser', replace),
|
||||
createChatWindow: (query) => ipcRenderer.send('create-chat-window', query),
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user