diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 01abe6c6..de7f58c3 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -2,11 +2,12 @@ import React, { useEffect, useState } from 'react'; import LauncherWindow from './LauncherWindow'; import ChatWindow from './ChatWindow'; import ErrorScreen from './components/ErrorScreen'; +import FeatureFlagsWindow from './components/FeatureFlags'; export default function App() { const [fatalError, setFatalError] = useState(null); const searchParams = new URLSearchParams(window.location.search); - const isLauncher = searchParams.get('window') === 'launcher'; + const targetWindow = searchParams.get('window'); useEffect(() => { const handleFatalError = (_: any, errorMessage: string) => { @@ -25,5 +26,12 @@ export default function App() { return window.electron.reloadApp()} />; } - return isLauncher ? : ; + + if (targetWindow === 'launcher') { + return ; + } else if (targetWindow === 'featureFlags') { + return ; + } else { + return ; + } } \ No newline at end of file diff --git a/ui/desktop/src/FeatureFlagsWindow.ts b/ui/desktop/src/FeatureFlagsWindow.ts new file mode 100644 index 00000000..30360de5 --- /dev/null +++ b/ui/desktop/src/FeatureFlagsWindow.ts @@ -0,0 +1,46 @@ +import { BrowserWindow } from 'electron'; +import path from 'node:path'; + +declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; +declare const MAIN_WINDOW_VITE_NAME: string; + +let featureFlagsWindow: BrowserWindow | null = null; + +export const createFeatureFlagsWindow = () => { + // Don't create multiple windows + if (featureFlagsWindow) { + featureFlagsWindow.focus(); + return; + } + + featureFlagsWindow = new BrowserWindow({ + width: 800, + height: 700, + title: 'Feature Flags', + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + }, + titleBarStyle: 'hidden', + trafficLightPosition: { x: 20, y: 20 }, + }); + + const launcherParams = '?window=featureFlags'; + if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { + featureFlagsWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${launcherParams}`); + } else { + featureFlagsWindow.loadFile( + path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html${launcherParams}`) + ); + } + + featureFlagsWindow.on('closed', () => { + featureFlagsWindow = null; + }); + + // Log any load failures + featureFlagsWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => { + console.error('Failed to load feature flags window:', errorCode, errorDescription); + }); +}; \ No newline at end of file diff --git a/ui/desktop/src/components/FeatureFlags.tsx b/ui/desktop/src/components/FeatureFlags.tsx new file mode 100644 index 00000000..f1d0a18a --- /dev/null +++ b/ui/desktop/src/components/FeatureFlags.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { featureFlags, type FeatureFlags } from '../featureFlags'; +import { Card } from './ui/card'; +import { Input } from './ui/input'; +import Box from './ui/Box'; + +export default function FeatureFlagsWindow() { + const [flags, setFlags] = React.useState(featureFlags.getFlags()); + + const handleFlagChange = (key: keyof FeatureFlags, value: any) => { + featureFlags.updateFlag(key, value); + setFlags({ ...featureFlags.getFlags() }); + }; + + return ( +
+ {/* Draggable title bar */} +
+ +
+
+
+ +
+

Feature Flags

+

+ Configure experimental features and settings +

+
+
+ + +
+ {Object.entries(flags).map(([key, value]) => ( +
+
+
+ + + {key.split(/(?=[A-Z])/).join(" ")} + +
+ {typeof value === 'boolean' ? ( + + ) : ( + handleFlagChange(key as keyof FeatureFlags, e.target.value)} + className="max-w-[300px] h-7 text-xs" + /> + )} +
+

+ {getFeatureFlagDescription(key)} +

+
+ ))} +
+
+ + +
+ +

+ ⚡️ Tip: Open a new chat window to see your changes take effect +

+
+
+
+
+
+ ); +} + +function getFeatureFlagDescription(key: string): string { + const descriptions: Record = { + whatCanGooseDoText: "Customize the splash screen button text", + expandedToolsByDefault: "Show tool outputs expanded by default instead of collapsed" + }; + return descriptions[key] || "No description available"; +} \ No newline at end of file diff --git a/ui/desktop/src/components/Splash.tsx b/ui/desktop/src/components/Splash.tsx index 1aa60efc..1f94b9f9 100644 --- a/ui/desktop/src/components/Splash.tsx +++ b/ui/desktop/src/components/Splash.tsx @@ -1,8 +1,11 @@ import React from 'react'; import GooseSplashLogo from './GooseSplashLogo'; import SplashPills from './SplashPills'; +import { featureFlags, type FeatureFlags } from '../featureFlags'; export default function Splash({ append }) { + const whatCanGooseDoText = featureFlags.getFlags().whatCanGooseDoText; + return (
@@ -17,13 +20,13 @@ export default function Splash({ append }) { className="w-[312px] px-16 py-4 text-14 text-center text-splash-pills-text dark:text-splash-pills-text-dark whitespace-nowrap cursor-pointer bg-prev-goose-gradient dark:bg-dark-prev-goose-gradient text-prev-goose-text dark:text-prev-goose-text-dark rounded-[14px] inline-block hover:scale-[1.02] transition-all duration-150" onClick={async () => { const message = { - content: "What can Goose do?", + content: whatCanGooseDoText, role: "user", }; await append(message); }} > - What can goose do? + {whatCanGooseDoText}
diff --git a/ui/desktop/src/components/ToolInvocations.tsx b/ui/desktop/src/components/ToolInvocations.tsx index 6ee436e9..4dfbcb30 100644 --- a/ui/desktop/src/components/ToolInvocations.tsx +++ b/ui/desktop/src/components/ToolInvocations.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Card } from './ui/card'; -import Box from './ui/Box' +import Box from './ui/Box'; +import { featureFlags } from '../featureFlags'; import { ToolCallArguments } from "./ToolCallArguments" import MarkdownContent from './MarkdownContent' import { snakeToTitleCase } from '../utils' @@ -88,8 +89,16 @@ interface ToolResultProps { } function ToolResult({ result }: ToolResultProps) { - // State to track expanded items - const [expandedItems, setExpandedItems] = React.useState([]); + const expandedToolsByDefault = featureFlags.getFlags().expandedToolsByDefault; + + // Initialize expanded state based on feature flag + const [expandedItems, setExpandedItems] = React.useState(() => { + if (expandedToolsByDefault) { + // If flag is on, start with all items expanded + return result?.result ? Array.from(Array(Array.isArray(result.result) ? result.result.length : 1).keys()) : []; + } + return []; // If flag is off, start with all items collapsed + }); // If no result info, don't show anything if (!result || !result.result) return null; diff --git a/ui/desktop/src/featureFlags.ts b/ui/desktop/src/featureFlags.ts new file mode 100644 index 00000000..8e3d95f6 --- /dev/null +++ b/ui/desktop/src/featureFlags.ts @@ -0,0 +1,68 @@ +interface FeatureFlags { + whatCanGooseDoText: string; + expandedToolsByDefault: boolean; +} + +class FeatureFlagsManager { + private static instance: FeatureFlagsManager; + private flags: FeatureFlags; + private readonly STORAGE_KEY = 'goose-feature-flags'; + + private constructor() { + // Define default flags + const defaultFlags: FeatureFlags = { + whatCanGooseDoText: "What can goose do?", + expandedToolsByDefault: false, + }; + + // Load flags from storage + const savedFlags = this.loadFlags(); + + // Create a new flags object starting with default values + this.flags = { ...defaultFlags }; + + // Only override with saved values for keys that exist in default flags + Object.keys(defaultFlags).forEach((key) => { + const typedKey = key as keyof FeatureFlags; + if (savedFlags.hasOwnProperty(key)) { + this.flags[typedKey] = savedFlags[typedKey]; + } + }); + } + + private loadFlags(): Partial { + try { + const saved = localStorage.getItem(this.STORAGE_KEY); + return saved ? JSON.parse(saved) : {}; + } catch { + return {}; + } + } + + private saveFlags(): void { + try { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.flags)); + } catch (error) { + console.error('Failed to save feature flags:', error); + } + } + + public static getInstance(): FeatureFlagsManager { + if (!FeatureFlagsManager.instance) { + FeatureFlagsManager.instance = new FeatureFlagsManager(); + } + return FeatureFlagsManager.instance; + } + + public getFlags(): FeatureFlags { + return this.flags; + } + + public updateFlag(key: K, value: FeatureFlags[K]): void { + this.flags[key] = value; + this.saveFlags(); + } +} + +export const featureFlags = FeatureFlagsManager.getInstance(); +export type { FeatureFlags }; \ No newline at end of file diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index f8240604..969e586a 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -7,6 +7,7 @@ import started from "electron-squirrel-startup"; import log from './utils/logger'; import { exec } from 'child_process'; import { addRecentDir, loadRecentDirs } from './utils/recentDirs'; +import { createFeatureFlagsWindow } from './FeatureFlagsWindow'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) app.quit(); @@ -335,6 +336,14 @@ app.whenReady().then(async () => { } + // Add Feature Flags menu item + fileMenu?.submenu?.append(new MenuItem({ + label: 'Feature Flags', + click() { + createFeatureFlagsWindow(); + }, + })); + Menu.setApplicationMenu(menu); app.on('activate', () => {