mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
[feat] "Wing to wing" mode
Co-authored-by: Mic Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -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: {},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 633 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
+15
-20
@@ -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 (
|
||||
<div className="relative w-screen h-screen overflow-hidden bg-gradient-to-b from-white to-gray-50 flex flex-col">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/chat/:id"
|
||||
element={<Chat key={selectedChatId} chats={chats} setChats={setChats} selectedChatId={selectedChatId} setSelectedChatId={setSelectedChatId} />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/chat/1" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
// TODO - Look at three separate renderers for this
|
||||
if (isLauncher) {
|
||||
return <LauncherWindow />;
|
||||
} else if (isWingToWing) {
|
||||
return <WingToWingWindow />;
|
||||
} else {
|
||||
return <ChatWindow />;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col w-screen h-screen bg-window-gradient items-center justify-center p-[10px]">
|
||||
<Tabs chats={chats} selectedChatId={selectedChatId} setSelectedChatId={setSelectedChatId} setChats={setChats} />
|
||||
|
||||
<Card className="flex flex-col flex-1 h-[calc(100vh-95px)] w-full bg-card-gradient mt-0 border-none shadow-xl rounded-2xl rounded-tl-none">
|
||||
{messages.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<div className="flex flex-1 items-center">
|
||||
<GooseSplashLogo />
|
||||
<span className="ask-goose-type ml-[8px]">ask<br />goose</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SplashPills append={append} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="flex-1 px-[10px]">
|
||||
<div className="block h-10" />
|
||||
{messages.map((message) => (
|
||||
<div key={message.id}>
|
||||
{message.role === 'user' ? (
|
||||
<UserMessage message={message} />
|
||||
) : (
|
||||
<GooseMessage message={message} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="block h-10" />
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<Input handleSubmit={handleSubmit} handleInputChange={handleInputChange} input={input} />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<React.SetStateAction<Chat[]>>,
|
||||
selectedChatId: number,
|
||||
setSelectedChatId: React.Dispatch<React.SetStateAction<number>>
|
||||
}) {
|
||||
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 (
|
||||
<div className="flex flex-col w-screen h-screen bg-window-gradient items-center justify-center p-[10px]">
|
||||
<Tabs
|
||||
chats={chats}
|
||||
selectedChatId={selectedChatId}
|
||||
setSelectedChatId={setSelectedChatId}
|
||||
setChats={setChats}
|
||||
/>
|
||||
|
||||
<Card className="flex flex-col flex-1 h-[calc(100vh-95px)] w-full bg-card-gradient mt-0 border-none shadow-xl rounded-2xl rounded-tl-none">
|
||||
{messages.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center">
|
||||
<div className="flex flex-1 items-center">
|
||||
<GooseSplashLogo />
|
||||
<span className="ask-goose-type ml-[8px]">ask<br />goose</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SplashPills append={append} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="flex-1 px-[10px]">
|
||||
<div className="block h-10" />
|
||||
{messages.map((message) => (
|
||||
<div key={message.id}>
|
||||
{message.role === 'user' ? (
|
||||
<UserMessage message={message} />
|
||||
) : (
|
||||
<GooseMessage message={message} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="block h-10" />
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<Input
|
||||
handleSubmit={handleSubmit}
|
||||
handleInputChange={handleInputChange}
|
||||
input={input}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<Chat[]>(() => {
|
||||
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 (
|
||||
<div className="relative w-screen h-screen overflow-hidden bg-gradient-to-b from-white to-gray-50 flex flex-col">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/chat/:id"
|
||||
element={
|
||||
<ChatContent
|
||||
key={selectedChatId}
|
||||
chats={chats}
|
||||
setChats={setChats}
|
||||
selectedChatId={selectedChatId}
|
||||
setSelectedChatId={setSelectedChatId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/chat/1" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLInputElement>(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 (
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-transparent overflow-hidden">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="w-[600px] bg-white/80 backdrop-blur-lg rounded-lg shadow-lg p-4"
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 = () => (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="fixed inset-0 p-[10px] overflow-auto bg-window-gradient">
|
||||
<Card className="w-full h-full flex flex-col items-center justify-center bg-card-gradient border-none shadow-xl rounded-2xl">
|
||||
{isInitialLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : lastToolInvocation && !isFinished ? (
|
||||
<div className="w-full h-full relative">
|
||||
<ToolInvocation
|
||||
key={lastToolInvocation.toolCallId}
|
||||
toolInvocation={lastToolInvocation}
|
||||
/>
|
||||
</div>
|
||||
) : isFinished ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-6">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<div className="text-lg font-medium text-gray-700">Goose has landed</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenChat}
|
||||
className="px-8 py-4 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition-all duration-200 text-lg font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
|
||||
>
|
||||
Continue in Goose
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-600 text-lg">Goose is getting a running start...</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,13 +6,16 @@ import ReactMarkdown from 'react-markdown'
|
||||
|
||||
export default function ToolInvocation({ toolInvocation }) {
|
||||
return (
|
||||
<div key={toolInvocation.toolCallId} className="w-full text-tool space-y-4 transition-all duration-300">
|
||||
<div key={toolInvocation.toolCallId} className="w-full h-full text-tool space-y-4 transition-all duration-300">
|
||||
<ToolCall call={toolInvocation} />
|
||||
{toolInvocation.state === 'result' && <ToolResult result={toolInvocation} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
interface ToolCallProps {
|
||||
call: {
|
||||
state: 'call' | 'result'
|
||||
@@ -31,7 +34,7 @@ function ToolCall({ call }: ToolCallProps) {
|
||||
</div>
|
||||
|
||||
{call.args && (
|
||||
<pre className="mt-1 text-tool-result-green">
|
||||
<pre className="mt-1 text-tool-result-green whitespace-pre-wrap">
|
||||
{JSON.stringify(call.args, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
+170
-48
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user