diff --git a/ui/desktop/src/LauncherWindow.tsx b/ui/desktop/src/LauncherWindow.tsx index c18916430..0d5b0589b 100644 --- a/ui/desktop/src/LauncherWindow.tsx +++ b/ui/desktop/src/LauncherWindow.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useRef, useState } from 'react'; declare global { interface Window { diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx index 2809188dc..efa7c2fb4 100644 --- a/ui/desktop/src/components/BottomMenu.tsx +++ b/ui/desktop/src/components/BottomMenu.tsx @@ -17,4 +17,4 @@ export default function BottomMenu({hasMessages}) { ); -} +} \ No newline at end of file diff --git a/ui/desktop/src/components/MoreMenu.tsx b/ui/desktop/src/components/MoreMenu.tsx index ee763ab58..e0a69ab35 100644 --- a/ui/desktop/src/components/MoreMenu.tsx +++ b/ui/desktop/src/components/MoreMenu.tsx @@ -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(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 ( @@ -84,52 +123,87 @@ export default function MoreMenu() { - -
-
- Use System Theme - -
- {!useSystemTheme && (
- {isDarkMode ? 'Dark Mode' : 'Light Mode'} + + +
+
+ Use System Theme + +
+ {!useSystemTheme && (
+ {isDarkMode ? 'Dark Mode' : 'Light Mode'} + +
)} + + {/* Versions Menu */} + {versions && versions.available_versions.length > 0 && ( + <> + + {showVersions && ( +
+ {versions.available_versions.map((version) => ( + + ))} +
+ )} + + )} + -
)} - - -
- + +
+
+
); } \ No newline at end of file diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 749973702..041c8b1be 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -1,19 +1,14 @@ -import path from 'node:path'; -import { execSync, spawn } from 'child_process'; +import { spawn } from 'child_process'; +import { createServer } from 'net'; +import os from 'node:os'; import { getBinaryPath } from './utils/binaryPath'; -import { existsSync } from 'fs'; import log from './utils/logger'; -import os from 'node:os'; -import { createServer } from 'net'; -import { loadZshEnv } from './utils/loadEnv'; - // Find an available port to start goosed on export const findAvailablePort = (): Promise => { 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 => { }); }; +// Function to fetch agent version from the server +const fetchAgentVersion = async (port: number): Promise => { + 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 => { +const checkServerStatus = async (port: number, maxAttempts: number = 60, interval: number = 100): Promise => { 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(); }); - log.info(`Goosed server successfully started on port ${port}`); - return [port, dir]; -}; - + // 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, agentVersion]; +}; \ No newline at end of file diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 39acfc81c..1f0d716ec 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -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(); -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) => { diff --git a/ui/desktop/src/preload.js b/ui/desktop/src/preload.js index 0d89a8ff6..1bb96ad4f 100644 --- a/ui/desktop/src/preload.js +++ b/ui/desktop/src/preload.js @@ -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),