mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat(ui): bring back quick launcher
This commit is contained in:
+18
-2
@@ -28,6 +28,7 @@ import ProviderSettings from './components/settings/providers/ProviderSettingsPa
|
||||
import { AppLayout } from './components/Layout/AppLayout';
|
||||
import { ChatProvider } from './contexts/ChatContext';
|
||||
import { DraftProvider } from './contexts/DraftContext';
|
||||
import LauncherView from './components/LauncherView';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useConfig } from './components/ConfigContext';
|
||||
@@ -89,8 +90,7 @@ const PairRouteWrapper = ({
|
||||
const routeState =
|
||||
(location.state as PairRouteState) || (window.history.state as PairRouteState) || {};
|
||||
const [searchParams] = useSearchParams();
|
||||
const [initialMessage] = useState(routeState.initialMessage);
|
||||
|
||||
const initialMessage = routeState.initialMessage;
|
||||
const resumeSessionId = searchParams.get('resumeSessionId') ?? undefined;
|
||||
|
||||
return (
|
||||
@@ -485,6 +485,21 @@ export function AppInner() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle initial message from launcher
|
||||
useEffect(() => {
|
||||
const handleSetInitialMessage = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
||||
const initialMessage = args[0] as string;
|
||||
if (initialMessage) {
|
||||
console.log('Received initial message from launcher:', initialMessage);
|
||||
navigate('/pair', { state: { initialMessage } });
|
||||
}
|
||||
};
|
||||
window.electron.on('set-initial-message', handleSetInitialMessage);
|
||||
return () => {
|
||||
window.electron.off('set-initial-message', handleSetInitialMessage);
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
if (fatalError) {
|
||||
return <ErrorUI error={new Error(fatalError)} />;
|
||||
}
|
||||
@@ -510,6 +525,7 @@ export function AppInner() {
|
||||
<div className="relative w-screen h-screen overflow-hidden bg-background-muted flex flex-col">
|
||||
<div className="titlebar-drag-region" />
|
||||
<Routes>
|
||||
<Route path="launcher" element={<LauncherView />} />
|
||||
<Route
|
||||
path="welcome"
|
||||
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
export default function LauncherView() {
|
||||
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
|
||||
const workingDir = window.appConfig?.get('GOOSE_WORKING_DIR') as string;
|
||||
window.electron.createChatWindow(query, workingDir);
|
||||
setQuery('');
|
||||
// Don't manually close - the blur handler will close the launcher when the new window takes focus
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
// Close on Escape
|
||||
if (e.key === 'Escape') {
|
||||
window.electron.closeWindow();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-transparent overflow-hidden">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="w-[600px] bg-background-default/95 backdrop-blur-lg rounded-lg shadow-2xl border border-border-default"
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full bg-transparent text-text-default text-xl px-6 py-4 outline-none placeholder-text-muted rounded-lg"
|
||||
placeholder="Ask goose anything..."
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+82
-25
@@ -10,6 +10,7 @@ import {
|
||||
MenuItem,
|
||||
Notification,
|
||||
powerSaveBlocker,
|
||||
screen,
|
||||
session,
|
||||
shell,
|
||||
Tray,
|
||||
@@ -502,7 +503,7 @@ const windowPowerSaveBlockers = new Map<number, number>(); // windowId -> blocke
|
||||
|
||||
const createChat = async (
|
||||
app: App,
|
||||
_query?: string,
|
||||
initialMessage?: string,
|
||||
dir?: string,
|
||||
_version?: string,
|
||||
resumeSessionId?: string,
|
||||
@@ -703,6 +704,13 @@ const createChat = async (
|
||||
log.info('Opening URL: ', formattedUrl);
|
||||
mainWindow.loadURL(formattedUrl);
|
||||
|
||||
// If we have an initial message, send it to the window once it's ready
|
||||
if (initialMessage) {
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
mainWindow.webContents.send('set-initial-message', initialMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Set up local keyboard shortcuts that only work when the window is focused
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
if (input.key === 'r' && input.meta) {
|
||||
@@ -793,6 +801,65 @@ const createChat = async (
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
const createLauncher = () => {
|
||||
const launcherWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 80,
|
||||
frame: false,
|
||||
transparent: process.platform === 'darwin',
|
||||
backgroundColor: process.platform === 'darwin' ? '#00000000' : '#ffffff',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
additionalArguments: [JSON.stringify(appConfig)],
|
||||
partition: 'persist:goose',
|
||||
},
|
||||
skipTaskbar: true,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
movable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
hasShadow: true,
|
||||
vibrancy: process.platform === 'darwin' ? 'window' : undefined,
|
||||
});
|
||||
|
||||
// Center on screen
|
||||
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 url = MAIN_WINDOW_VITE_DEV_SERVER_URL
|
||||
? new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
|
||||
: pathToFileURL(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
|
||||
|
||||
url.hash = '/launcher';
|
||||
launcherWindow.loadURL(formatUrl(url));
|
||||
|
||||
// Destroy window when it loses focus
|
||||
launcherWindow.on('blur', () => {
|
||||
launcherWindow.destroy();
|
||||
});
|
||||
|
||||
// Also destroy on escape key
|
||||
launcherWindow.webContents.on('before-input-event', (event, input) => {
|
||||
if (input.key === 'Escape') {
|
||||
launcherWindow.destroy();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
return launcherWindow;
|
||||
};
|
||||
|
||||
// Track tray instance
|
||||
let tray: Tray | null = null;
|
||||
|
||||
@@ -1659,28 +1726,6 @@ const focusWindow = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const registerGlobalHotkey = (accelerator: string) => {
|
||||
// Unregister any existing shortcuts first
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
try {
|
||||
globalShortcut.register(accelerator, () => {
|
||||
focusWindow();
|
||||
});
|
||||
|
||||
// Check if the shortcut was registered successfully
|
||||
if (globalShortcut.isRegistered(accelerator)) {
|
||||
return true;
|
||||
} else {
|
||||
console.error('Failed to register global hotkey');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error registering global hotkey:', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
async function appMain() {
|
||||
// Ensure Windows shims are available before any MCP processes are spawned
|
||||
await ensureWinShims();
|
||||
@@ -1736,8 +1781,13 @@ async function appMain() {
|
||||
});
|
||||
});
|
||||
|
||||
// Register the default global hotkey
|
||||
registerGlobalHotkey('CommandOrControl+Alt+Shift+G');
|
||||
try {
|
||||
globalShortcut.register('CommandOrControl+Alt+Shift+G', () => {
|
||||
createLauncher();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error registering launcher hotkey:', e);
|
||||
}
|
||||
|
||||
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
details.requestHeaders['Origin'] = 'http://localhost:5173';
|
||||
@@ -1990,6 +2040,13 @@ async function appMain() {
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on('close-window', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
if (window && !window.isDestroyed()) {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('notify', (_event, data) => {
|
||||
try {
|
||||
// Validate notification data
|
||||
|
||||
+18
-13
@@ -8,20 +8,25 @@ import { client } from './api/client.gen';
|
||||
const App = lazy(() => import('./App'));
|
||||
|
||||
(async () => {
|
||||
console.log('window created, getting goosed connection info');
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
if (baseUrl === null) {
|
||||
window.alert('failed to start goose backend process');
|
||||
return;
|
||||
// Check if we're in the launcher view (doesn't need goosed connection)
|
||||
const isLauncher = window.location.hash === '#/launcher';
|
||||
|
||||
if (!isLauncher) {
|
||||
console.log('window created, getting goosed connection info');
|
||||
const baseUrl = await window.electron.getGoosedHostPort();
|
||||
if (baseUrl === null) {
|
||||
window.alert('failed to start goose backend process');
|
||||
return;
|
||||
}
|
||||
console.log('connecting at', baseUrl);
|
||||
client.setConfig({
|
||||
baseUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': await window.electron.getSecretKey(),
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log('connecting at', baseUrl);
|
||||
client.setConfig({
|
||||
baseUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': await window.electron.getSecretKey(),
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
|
||||
Reference in New Issue
Block a user