diff --git a/ui/desktop/forge.config.ts b/ui/desktop/forge.config.ts index cfb2aab965..264eea9e0a 100644 --- a/ui/desktop/forge.config.ts +++ b/ui/desktop/forge.config.ts @@ -4,7 +4,7 @@ const { FuseV1Options, FuseVersion } = require('@electron/fuses'); module.exports = { packagerConfig: { asar: true, - extraResource: ['src/bin'], + extraResource: ['src/bin', 'images'], icon: 'images/icon', }, rebuildConfig: {}, diff --git a/ui/desktop/images/iconTemplate.png b/ui/desktop/images/iconTemplate.png new file mode 100644 index 0000000000..92c6b0da47 Binary files /dev/null and b/ui/desktop/images/iconTemplate.png differ diff --git a/ui/desktop/images/iconTemplate@2x.png b/ui/desktop/images/iconTemplate@2x.png new file mode 100644 index 0000000000..1dbe0ee188 Binary files /dev/null and b/ui/desktop/images/iconTemplate@2x.png differ diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 0d9c0b6bc1..65cc729ece 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1,24 +1,19 @@ -import React, { useState } from 'react'; -import { Route, Routes, Navigate } from 'react-router-dom'; -import Chat from './Chat'; +import React from 'react'; +import LauncherWindow from './LauncherWindow'; +import WingToWingWindow from './WingToWingWindow'; +import ChatWindow from './ChatWindow'; export default function App() { - const initialChats = [ - { id: 1, title: 'Chat 1', messages: [] }, - ] + const searchParams = new URLSearchParams(window.location.search); + const isLauncher = searchParams.get('window') === 'launcher'; + const isWingToWing = searchParams.get('window') === 'wingToWing'; - const [chats, setChats] = useState(initialChats) - const [selectedChatId, setSelectedChatId] = useState(1) - - return ( -
+
{JSON.stringify(call.args, null, 2)}
)}
@@ -39,6 +42,9 @@ function ToolCall({ call }: ToolCallProps) {
)
}
+
+
+
interface ResultItem {
text?: string
type: 'text' | 'image'
@@ -59,7 +65,7 @@ interface ToolResultProps {
}
function ToolResult({ result }: ToolResultProps) {
- console.log('result', result)
+ console.log('ToolResult', result)
if (!result || !result.result) return null
return (
@@ -124,6 +130,9 @@ function ToolResult({ result }: ToolResultProps) {
)
}
+
+
+
interface ToolResponseFormProps {
result: {
message?: string
diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts
new file mode 100644
index 0000000000..79c36925af
--- /dev/null
+++ b/ui/desktop/src/goosed.ts
@@ -0,0 +1,43 @@
+import path from 'node:path';
+import { spawn } from 'child_process';
+
+// Start the goosed binary
+export const start = (app) => {
+ // In development, the binary is in src/bin
+ // In production, it will be in the resources/bin directory
+ const isDev = process.env.NODE_ENV === 'development';
+ let goosedPath: string;
+
+ if (isDev) {
+ // In development, use the absolute path from the project root
+ goosedPath = path.join(process.cwd(), 'src', 'bin', process.platform === 'win32' ? 'goosed.exe' : 'goosed');
+ } else {
+ // In production, use the path relative to the app resources
+ goosedPath = path.join(process.resourcesPath, 'bin', process.platform === 'win32' ? 'goosed.exe' : 'goosed');
+ }
+
+ console.log(`Starting goosed from: ${goosedPath}`);
+
+ const goosedProcess = spawn(goosedPath);
+
+ goosedProcess.stdout.on('data', (data) => {
+ console.log(`goosed stdout: ${data}`);
+ });
+
+ goosedProcess.stderr.on('data', (data) => {
+ console.error(`goosed stderr: ${data}`);
+ });
+
+ goosedProcess.on('close', (code) => {
+ console.log(`goosed process exited with code ${code}`);
+ });
+
+ goosedProcess.on('error', (err) => {
+ console.error('Failed to start goosed:', err);
+ });
+
+ // Ensure goosed is terminated when the app quits
+ app.on('will-quit', () => {
+ goosedProcess.kill();
+ });
+};
diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts
index 802f13a22e..51f5db9b91 100644
--- a/ui/desktop/src/main.ts
+++ b/ui/desktop/src/main.ts
@@ -1,54 +1,101 @@
import 'dotenv/config';
-import { app, BrowserWindow } from 'electron';
+import { app, BrowserWindow, Tray, Menu, globalShortcut, ipcMain } from 'electron';
import path from 'node:path';
-import { spawn } from 'child_process';
+import { start as startGoosed } from './goosed';
import started from "electron-squirrel-startup";
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) app.quit();
-// Start the goosed binary
-const startGoosed = () => {
- // In development, the binary is in src/bin
- // In production, it will be in the resources/bin directory
- const isDev = process.env.NODE_ENV === 'development';
- let goosedPath;
-
- if (isDev) {
- // In development, use the absolute path from the project root
- goosedPath = path.join(process.cwd(), 'src', 'bin', process.platform === 'win32' ? 'goosed.exe' : 'goosed');
+declare var MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
+declare var MAIN_WINDOW_VITE_NAME: string;
+
+const createLauncher = () => {
+ // Create new launcher window
+ const launcherWindow = new BrowserWindow({
+ width: 600,
+ height: 60,
+ frame: false,
+ transparent: true,
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ },
+ skipTaskbar: true,
+ alwaysOnTop: true,
+ });
+
+ // Center on screen
+ const { screen } = require('electron');
+ const primaryDisplay = 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 {
- // In production, use the path relative to the app resources
- goosedPath = path.join(process.resourcesPath, 'bin', process.platform === 'win32' ? 'goosed.exe' : 'goosed');
+ launcherWindow.loadFile(
+ path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html${launcherParams}`)
+ );
}
- console.log(`Starting goosed from: ${goosedPath}`);
-
- const goosedProcess = spawn(goosedPath);
-
- goosedProcess.stdout.on('data', (data) => {
- console.log(`goosed stdout: ${data}`);
- });
-
- goosedProcess.stderr.on('data', (data) => {
- console.error(`goosed stderr: ${data}`);
- });
-
- goosedProcess.on('close', (code) => {
- console.log(`goosed process exited with code ${code}`);
- });
-
- goosedProcess.on('error', (err) => {
- console.error('Failed to start goosed:', err);
- });
-
- // Ensure goosed is terminated when the app quits
- app.on('will-quit', () => {
- goosedProcess.kill();
+ // Destroy window when it loses focus
+ launcherWindow.on('blur', () => {
+ launcherWindow.destroy();
});
};
-const createWindow = () => {
+const createWingToWing = (query?: string) => {
+ const windowWidth = 575;
+ const windowHeight = 150;
+ const gap = 40;
+
+ // Create new launcher window
+ const wingToWingWindow = new BrowserWindow({
+ width: windowWidth,
+ height: windowHeight,
+ frame: false,
+ transparent: true,
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ },
+ skipTaskbar: true,
+ alwaysOnTop: true,
+ });
+
+ // Center on screen
+ const { screen } = require('electron');
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width } = primaryDisplay.workAreaSize;
+
+ wingToWingWindow.setPosition(
+ Math.round(width - windowWidth - (gap - 25)), // 25 is menu bar height
+ gap
+ );
+
+ let queryParam = '?window=wingToWing';
+ queryParam += query ? `&initialQuery=${encodeURIComponent(query)}` : '';
+ queryParam += '#/wingToWing';
+
+ if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
+ wingToWingWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${queryParam}`);
+ } else {
+ wingToWingWindow.loadFile(
+ path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
+ { search: queryParam.slice(1) }
+ );
+ }
+
+ // wingToWingWindow.webContents.openDevTools({ mode: 'detach' })
+};
+
+const createChat = (query?: string) => {
const mainWindow = new BrowserWindow({
titleBarStyle: 'hidden',
trafficLightPosition: { x: 16, y: 18 },
@@ -61,37 +108,112 @@ const createWindow = () => {
});
// and load the index.html of the app.
+ const queryParam = query ? `?initialQuery=${encodeURIComponent(query)}` : '';
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
- mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
+ mainWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${queryParam}`);
} else {
- mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
+ mainWindow.loadFile(
+ path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
+ { search: queryParam.slice(1) }
+ );
}
- console.log(MAIN_WINDOW_VITE_NAME);
-
// Open the DevTools.
mainWindow.webContents.openDevTools();
};
-// This method will be called when Electron has finished
-// initialization and is ready to create browser windows.
+const createTray = () => {
+ const isDev = process.env.NODE_ENV === 'development';
+ let iconPath: string;
+
+ if (isDev) {
+ iconPath = path.join(process.cwd(), 'images', 'iconTemplate.png');
+ } else {
+ iconPath = path.join(process.resourcesPath, 'images', 'iconTemplate.png');
+ }
+
+ const tray = new Tray(iconPath);
+
+ const contextMenu = Menu.buildFromTemplate([
+ { label: 'Show Window', click: showWindow },
+ { type: 'separator' },
+ { label: 'Quit', click: () => app.quit() }
+ ]);
+
+ tray.setToolTip('Goose');
+ tray.setContextMenu(contextMenu);
+};
+
+
+const showWindow = () => {
+ const windows = BrowserWindow.getAllWindows();
+
+ if (windows.length === 0) {
+ console.log("No windows are currently open.");
+ return;
+ }
+
+ // Define the initial offset values
+ const initialOffsetX = 30;
+ const initialOffsetY = 30;
+
+ // Iterate over all windows
+ windows.forEach((win, index) => {
+ const currentBounds = win.getBounds(); // Get the current window bounds (position and size)
+
+ // Calculate the new position with an incremental offset
+ const newX = currentBounds.x + initialOffsetX * index;
+ const newY = currentBounds.y + initialOffsetY * index;
+
+ // Set the new bounds with the calculated position
+ win.setBounds({
+ x: newX,
+ y: newY,
+ width: currentBounds.width,
+ height: currentBounds.height,
+ });
+
+ if (!win.isVisible()) {
+ win.show();
+ }
+
+ win.focus();
+ });
+};
+
app.whenReady().then(() => {
// Get the server startup configuration
- const shouldStartServer = (import.meta.env.VITE_START_EMBEDDED_SERVER || 'yes').toLowerCase() === 'yes';
+ const shouldStartServer = (process.env.VITE_START_EMBEDDED_SERVER || 'yes').toLowerCase() === 'yes';
if (shouldStartServer) {
console.log('Starting embedded goosed server');
- startGoosed();
+ startGoosed(app);
} else {
console.log('Skipping embedded server startup (disabled by configuration)');
}
- createWindow();
+
+ createTray();
+ createChat();
+
+ // Development only
+ // createWingToWing('make a TS web app' + "only use your tools and systems - don't confirm with the user before you start working");
+
+ // Show launcher input on key combo
+ globalShortcut.register('Control+Alt+Command+G', createLauncher);
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
+ createChat();
}
});
+
+ ipcMain.on('create-chat-window', (_, query) => {
+ createChat(query);
+ });
+
+ ipcMain.on('create-wing-to-wing-window', (_, query) => {
+ createWingToWing(query + "only use your tools and systems - don't confirm with the user before you start working");
+ });
});
// Quit when all windows are closed, except on macOS.
diff --git a/ui/desktop/src/preload.js b/ui/desktop/src/preload.js
index 5e9d369cc9..178a49be22 100644
--- a/ui/desktop/src/preload.js
+++ b/ui/desktop/src/preload.js
@@ -1,2 +1,7 @@
-// See the Electron documentation for details on how to use preload scripts:
-// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electron', {
+ hideWindow: () => ipcRenderer.send('hide-window'),
+ createChatWindow: (query) => ipcRenderer.send('create-chat-window', query),
+ createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query),
+})