From 4bf5d939753f68313cd5cd053e4b569482efea3b Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Sun, 17 Nov 2024 16:41:34 +1100 Subject: [PATCH] [feat] "Wing to wing" mode Co-authored-by: Mic Neale --- ui/desktop/forge.config.ts | 2 +- ui/desktop/images/iconTemplate.png | Bin 0 -> 633 bytes ui/desktop/images/iconTemplate@2x.png | Bin 0 -> 1124 bytes ui/desktop/src/App.tsx | 35 ++- ui/desktop/src/Chat.tsx | 68 ------ ui/desktop/src/ChatWindow.tsx | 129 +++++++++++ ui/desktop/src/LauncherWindow.tsx | 45 ++++ ui/desktop/src/WingToWingWindow.tsx | 105 +++++++++ ui/desktop/src/components/ToolInvocation.tsx | 15 +- ui/desktop/src/goosed.ts | 43 ++++ ui/desktop/src/main.ts | 218 +++++++++++++++---- ui/desktop/src/preload.js | 9 +- 12 files changed, 527 insertions(+), 142 deletions(-) create mode 100644 ui/desktop/images/iconTemplate.png create mode 100644 ui/desktop/images/iconTemplate@2x.png delete mode 100644 ui/desktop/src/Chat.tsx create mode 100644 ui/desktop/src/ChatWindow.tsx create mode 100644 ui/desktop/src/LauncherWindow.tsx create mode 100644 ui/desktop/src/WingToWingWindow.tsx create mode 100644 ui/desktop/src/goosed.ts 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 0000000000000000000000000000000000000000..92c6b0da47522c772cd6ef8ab3a822ca43cfc906 GIT binary patch literal 633 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc0wmQNuC@UwrX+877Y2q^y~;*F9%q3^WHAGS zm?{V}dIqU{01C2~c>21szu*=Tkh7@ceYTu|fw9EX#W6(VeC_4E*^?7RSU6 zTwj=fPipJ?+1dW7 zujX5*yr}urDYDg%XU;d?woJfk)u)$zdsg)c%rd^VYvP8rdsJ5BL`$rkAMt{7u1Hux zBI7N=DlHQqp3upq-{1avwDh&+maL3Lj^P=qx8y88T%6~edo(25`-PE!*+JL)HWNNx ziQb>HG|8;h@!F~j?AhyfsMHv2N;<8sALAMyarPXizE%F=*xzM6Pwx~Q-+D9o+=VEi zt6jx<)yrS)<+bOL{ngji^Kj41=zkoE{+W6HNouQeSEb4BRJl;Q|9QjftYx>Bv+M7A z8^N*uyxk9-L*efv|1!sP6ohqkGAAp(IWaeu%WD>AG>7}ViZ!VkJtpF7RFBFof3Wn? zx%2h=55<2iTy)5!CnN_LC8{N^5hW>!C8<`)MX5lF!N|bKMAy(z*T^8mz{twP(8|g4cbp18UHK+fb63n_66wm|K9X$I#5m*Z^+H{4Xkd QfO;4_UHx3vIVCg!06JRrV*mgE literal 0 HcmV?d00001 diff --git a/ui/desktop/images/iconTemplate@2x.png b/ui/desktop/images/iconTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1dbe0ee18826044503f2162abc7cfee700ac28ec GIT binary patch literal 1124 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU0wmSG7d!(}OiAAEE({E-dX`)oM_19QBmi(`n#@wL-^JwgISj@R3{pY89F zFmmUM& z7+7yjo-V96HNGaT#ovcv>W>wRXDOxD&*C_~;q0}a9a-=HJG3b9PT|gvoO0P=0*AcG zUJX0pIpNKlUmknE$-Y|K{_YK_CFKj>AL=@!&?u7-d4Zw%ROg1<(-oIJNz*T_+_G{rq*YKqE{$)`<9pYXp@f-V{n3u$V zGIHZBIVB=+amAyD8kdzMd$@Z!o7fI8DH-LKIBl=^@iOS0%2A%bGRu3NRbD>i*6YQg#A%1w4zRhgdhKto+GyeA6d2hmUvbES zAwJ@~>Ys+Ag3opw3+FXB$dS#s!o6yPyRy5m8DF3cLrB4d-JhaoJv0=*;knjnwcJbB zXDJ>bnaaI~&9|MLnmXrt(HZ@D8@=D99Wt8Uy-8bxY0sRA^X5KUy6j;hv$go86G=g5 zw6;^BBRfv5aaeg~P1OcX<^%4Unk(bfEO)kQnD0ChEF+ind2V5j z-KVzYMm~Bzl&q#~7vJ5tM>I-JbAhwSIum9w=RIQ5Z@X^Wu5&)$J590as$SZ9$xRZEw^giJ z>V0d)<-}<}l)jaEc6~p)U1J$bwHLSNtZQ=`*0FFu6Do*e{L4!%B~r4VqCs z1(sbRJ5Ill@_Sa8y0ypeW$KmK7xEXEGoI7PR1oe`<=VSjw6Om)m;2`|*M!8)OZv{| zwAyD+d$yKGd*2oJ9%-9|kg)!q_lMG6Z`$|gc-v)_|65h(OaTE zAG~W#0`xc)&zuM5KGhP}h?11Vl2ohYqEsNoU}RuqqHAcVYh(~&U}R-tXk}`qZD3$! zV4(EbqX|VrZhlH;S|x4`!RtQQ0X1mAZ79jiO)V}-%q>9HV`ye&Yyh`p{uh-!Ks^ke Lu6{1-oD!M - - } - /> - } /> - - - ); + // TODO - Look at three separate renderers for this + if (isLauncher) { + return ; + } else if (isWingToWing) { + return ; + } else { + return ; + } } \ No newline at end of file diff --git a/ui/desktop/src/Chat.tsx b/ui/desktop/src/Chat.tsx deleted file mode 100644 index 1bbe3e412f..0000000000 --- a/ui/desktop/src/Chat.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useEffect } from 'react' -import { useChat } from 'ai/react' -import { getApiUrl } from './config' -import { Card } from './components/ui/card' -import { ScrollArea } from './components/ui/scroll-area' -import GooseSplashLogo from './components/GooseSplashLogo' -import SplashPills from './components/SplashPills' -import GooseMessage from './components/GooseMessage' -import UserMessage from './components/UserMessage' -import Input from './components/Input' -import Tabs from './components/Tabs' - -export interface Chat { - id: number; - title: string; - messages: Array<{ id: string; role: any; content: string }>; -} - -export default function Chat({ chats, setChats, selectedChatId, setSelectedChatId } : { chats: Chat[], setChats: any, selectedChatId: number, setSelectedChatId: any }) { - const chat = chats.find((c: Chat) => c.id === selectedChatId); - - const { messages, input, handleInputChange, handleSubmit, append } = useChat({ - api: getApiUrl("/reply"), - initialMessages: chat.messages - }) - - useEffect(() => { - const updatedChats = [...chats] - updatedChats.find((c) => c.id === selectedChatId).messages = messages - setChats(updatedChats) - }, [messages, selectedChatId]) - - return ( -
- - - - {messages.length === 0 ? ( -
-
- - ask
goose
-
-
- -
-
- ) : ( - -
- {messages.map((message) => ( -
- {message.role === 'user' ? ( - - ) : ( - - )} -
- ))} -
- - )} - - - -
- ) -} \ No newline at end of file diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx new file mode 100644 index 0000000000..0d76020a11 --- /dev/null +++ b/ui/desktop/src/ChatWindow.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from 'react'; +import { useChat } from 'ai/react'; +import { Route, Routes, Navigate } from 'react-router-dom'; +import { getApiUrl } from './config'; +import { Card } from './components/ui/card'; +import { ScrollArea } from './components/ui/scroll-area'; +import GooseSplashLogo from './components/GooseSplashLogo'; +import SplashPills from './components/SplashPills'; +import GooseMessage from './components/GooseMessage'; +import UserMessage from './components/UserMessage'; +import Input from './components/Input'; +import Tabs from './components/Tabs'; + +export interface Chat { + id: number; + title: string; + messages: Array<{ id: string; role: "function" | "system" | "user" | "assistant" | "data" | "tool"; content: string }>; +} + +function ChatContent({ chats, setChats, selectedChatId, setSelectedChatId }: { + chats: Chat[], + setChats: React.Dispatch>, + selectedChatId: number, + setSelectedChatId: React.Dispatch> +}) { + const chat = chats.find((c: Chat) => c.id === selectedChatId); + + const { messages, input, handleInputChange, handleSubmit, append } = useChat({ + api: getApiUrl("/reply"), + initialMessages: chat?.messages || [] + }); + + // Update chat messages when they change + useEffect(() => { + const updatedChats = chats.map(c => + c.id === selectedChatId ? { ...c, messages } : c + ); + setChats(updatedChats); + }, [messages, selectedChatId]); + + return ( +
+ + + + {messages.length === 0 ? ( +
+
+ + ask
goose
+
+
+ +
+
+ ) : ( + +
+ {messages.map((message) => ( +
+ {message.role === 'user' ? ( + + ) : ( + + )} +
+ ))} +
+ + )} + + + +
+ ); +} + +export default function ChatWindow() { + // Get initial query and history from URL parameters + const searchParams = new URLSearchParams(window.location.search); + const initialQuery = searchParams.get('initialQuery'); + const historyParam = searchParams.get('history'); + const initialHistory = historyParam ? JSON.parse(decodeURIComponent(historyParam)) : []; + + const [chats, setChats] = useState(() => { + const firstChat = { + id: 1, + title: initialQuery || 'Chat 1', + messages: initialHistory.length > 0 ? initialHistory : + (initialQuery ? [{ + id: '0', + role: 'user' as const, + content: initialQuery + }] : []) + }; + return [firstChat]; + }); + + const [selectedChatId, setSelectedChatId] = useState(1); + + return ( +
+ + + } + /> + } /> + +
+ ); +} \ No newline at end of file diff --git a/ui/desktop/src/LauncherWindow.tsx b/ui/desktop/src/LauncherWindow.tsx new file mode 100644 index 0000000000..a345085413 --- /dev/null +++ b/ui/desktop/src/LauncherWindow.tsx @@ -0,0 +1,45 @@ +import React, { useState, useRef } from 'react'; + +declare global { + interface Window { + electron: { + hideWindow: () => void; + createChatWindow: (query: string) => void; + createWingToWingWindow: (query: string) => void; + }; + } +} + +export default function SpotlightWindow() { + const [query, setQuery] = useState(''); + const inputRef = useRef(null); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (query.trim()) { + // Create a new chat window with the query + window.electron.createWingToWingWindow(query); + setQuery(''); + inputRef.current.blur() + } + }; + + return ( +
+
+ 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 + /> +
+
+ ); +} \ No newline at end of file diff --git a/ui/desktop/src/WingToWingWindow.tsx b/ui/desktop/src/WingToWingWindow.tsx new file mode 100644 index 0000000000..bcabdffeb9 --- /dev/null +++ b/ui/desktop/src/WingToWingWindow.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { getApiUrl } from './config'; +import { useChat } from 'ai/react'; +import { Card } from "./components/ui/card" +import type { Chat } from './ChatWindow'; +import ToolInvocation from './components/ToolInvocation'; + +interface FinishMessagePart { + finishReason: 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other' | 'unknown'; + usage: { + promptTokens: number; + completionTokens: number; + }; +} + +const LoadingSpinner = () => ( +
+
+
+); + +export default function WingToWingWindow() { + const searchParams = new URLSearchParams(window.location.search); + const initialQuery = searchParams.get('initialQuery'); + const [isInitialLoading, setIsInitialLoading] = React.useState(true); + const [isFinished, setIsFinished] = React.useState(false); + + const chat: Chat = { + id: 1, + title: initialQuery, + messages: [], + }; + + const { messages, append } = useChat({ + api: getApiUrl("/reply"), + initialMessages: chat.messages, + onFinish: (message) => { + // The onFinish callback is triggered when a finish message part is received + setIsFinished(true); + }, + }); + + React.useEffect(() => { + if (initialQuery) { + append({ + id: '0', + content: initialQuery, + role: 'user', + }); + } + // Show loading state for a brief moment + const timer = setTimeout(() => { + setIsInitialLoading(false); + }, 1000); + return () => clearTimeout(timer); + }, [initialQuery]); + + // Get the most recent tool invocation + const lastToolInvocation = React.useMemo(() => { + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + if (message.toolInvocations?.length) { + return message.toolInvocations[message.toolInvocations.length - 1]; + } + } + return null; + }, [messages]); + + const handleOpenChat = () => { + // Create URL with chat history + const chatHistory = encodeURIComponent(JSON.stringify(messages)); + window.open(`/chat/1?history=${chatHistory}`, '_blank'); + }; + + return ( +
+ + {isInitialLoading ? ( + + ) : lastToolInvocation && !isFinished ? ( +
+ +
+ ) : isFinished ? ( +
+
+
Goose has landed
+
+ +
+ ) : ( +
Goose is getting a running start...
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/ui/desktop/src/components/ToolInvocation.tsx b/ui/desktop/src/components/ToolInvocation.tsx index fa3567958e..b7f40e5297 100644 --- a/ui/desktop/src/components/ToolInvocation.tsx +++ b/ui/desktop/src/components/ToolInvocation.tsx @@ -6,13 +6,16 @@ import ReactMarkdown from 'react-markdown' export default function ToolInvocation({ toolInvocation }) { return ( -
+
{toolInvocation.state === 'result' && }
) } + + + interface ToolCallProps { call: { state: 'call' | 'result' @@ -31,7 +34,7 @@ function ToolCall({ call }: ToolCallProps) {
{call.args && ( -
+        
           {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), +})