Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scrappy version of feature flags for gui #452

Open
wants to merge 10 commits into
base: v1.0
Choose a base branch
from
12 changes: 10 additions & 2 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
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<string | null>(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) => {

Check warning on line 13 in ui/desktop/src/App.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
setFatalError(errorMessage);
};

Expand All @@ -25,5 +26,12 @@
return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />;
}

return isLauncher ? <LauncherWindow /> : <ChatWindow />;

if (targetWindow === 'launcher') {
return <LauncherWindow />;
} else if (targetWindow === 'featureFlags') {
return <FeatureFlagsWindow />;
} else {
return <ChatWindow />;
}
}
46 changes: 46 additions & 0 deletions ui/desktop/src/FeatureFlagsWindow.ts
Original file line number Diff line number Diff line change
@@ -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);
});
};
90 changes: 90 additions & 0 deletions ui/desktop/src/components/FeatureFlags.tsx
Original file line number Diff line number Diff line change
@@ -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>(featureFlags.getFlags());

const handleFlagChange = (key: keyof FeatureFlags, value: any) => {
featureFlags.updateFlag(key, value);
setFlags({ ...featureFlags.getFlags() });
};

return (
<div className="h-screen flex flex-col">
{/* Draggable title bar */}
<div className="h-12 w-full draggable" style={{ WebkitAppRegion: 'drag' }} />

<div className="flex-1 container max-w-2xl mx-auto py-4">
<div className="space-y-4">
<div className="flex items-center mb-2">
<Box size={16} />
<div className="ml-2">
<h1 className="text-lg font-semibold leading-none">Feature Flags</h1>
<p className="text-xs text-gray-500 mt-1">
Configure experimental features and settings
</p>
</div>
</div>

<Card className="p-4">
<div className="space-y-4">
{Object.entries(flags).map(([key, value]) => (
<div key={key} className="flex flex-col space-y-1">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Box size={12} />
<span className="ml-2 text-xs font-medium">
{key.split(/(?=[A-Z])/).join(" ")}
</span>
</div>
{typeof value === 'boolean' ? (
<button
onClick={() => handleFlagChange(key as keyof FeatureFlags, !value)}
className={`px-3 py-1 rounded text-xs font-medium ${
value
? 'bg-green-100 text-green-800 hover:bg-green-200'
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
}`}
>
{value ? 'Enabled' : 'Disabled'}
</button>
) : (
<Input
value={value}
onChange={(e) => handleFlagChange(key as keyof FeatureFlags, e.target.value)}
className="max-w-[300px] h-7 text-xs"
/>
)}
</div>
<p className="text-xs text-gray-500 ml-6">
{getFeatureFlagDescription(key)}
</p>
</div>
))}
</div>
</Card>

<Card className="p-3 bg-gray-50">
<div className="flex items-center">
<Box size={12} />
<p className="text-xs text-gray-500 ml-2">
⚡️ Tip: Open a new chat window to see your changes take effect
</p>
</div>
</Card>
</div>
</div>
</div>
);
}

function getFeatureFlagDescription(key: string): string {
const descriptions: Record<string, string> = {
whatCanGooseDoText: "Customize the splash screen button text",
expandedToolsByDefault: "Show tool outputs expanded by default instead of collapsed"
};
return descriptions[key] || "No description available";
}
7 changes: 5 additions & 2 deletions ui/desktop/src/components/Splash.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="h-full flex flex-col items-center justify-center">
<div className="flex flex-1" />
Expand All @@ -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}
</div>
<div className="flex flex-1" />
<div className={`mt-[10px] w-[198px] h-[17px] py-2 flex-col justify-center items-start inline-flex`}>
Expand Down
15 changes: 12 additions & 3 deletions ui/desktop/src/components/ToolInvocations.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -88,8 +89,16 @@ interface ToolResultProps {
}

function ToolResult({ result }: ToolResultProps) {
// State to track expanded items
const [expandedItems, setExpandedItems] = React.useState<number[]>([]);
const expandedToolsByDefault = featureFlags.getFlags().expandedToolsByDefault;

// Initialize expanded state based on feature flag
const [expandedItems, setExpandedItems] = React.useState<number[]>(() => {
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;
Expand Down
68 changes: 68 additions & 0 deletions ui/desktop/src/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -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)) {

Check failure on line 27 in ui/desktop/src/featureFlags.ts

View workflow job for this annotation

GitHub Actions / build

Do not access Object.prototype method 'hasOwnProperty' from target object
this.flags[typedKey] = savedFlags[typedKey];
}
});
}

private loadFlags(): Partial<FeatureFlags> {
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<K extends keyof FeatureFlags>(key: K, value: FeatureFlags[K]): void {
this.flags[key] = value;
this.saveFlags();
}
}

export const featureFlags = FeatureFlagsManager.getInstance();
export type { FeatureFlags };
9 changes: 9 additions & 0 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading