mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat(ui): add white-label configuration system
Adds a declarative whitelabel.yaml config file that controls the entire desktop app at build time. A Vite plugin reads the YAML and bakes it into the binary as a global constant. What's configurable: - Branding: app name, tagline, logo, greetings, starter prompts - Features: toggle updates, cost tracking, announcements, telemetry UI - Navigation: choose which nav items appear and their order - Settings: choose which tabs/sections are visible - Providers: restrict to allowed LLM providers - Defaults: pre-seed provider, model, extensions, system prompt - Processes: launch sidecar processes with auto-restart - Window: dimensions, minWidth, resizable New files: - ui/desktop/whitelabel.yaml (the config) - ui/desktop/whitelabel.example.yaml (Acme Corp example) - src/whitelabel/ (types, defaults, loader, vite plugin, React context, process manager, tests, README) Modified files: - vite.main.config.mts, vite.renderer.config.mts (plugin wiring) - App.tsx (WhiteLabelProvider wrapper) - updates.ts (feature flags driven by config) - main.ts (window dims, app name, process lifecycle) - Greeting.tsx (greetings from config) - NavigationPanel.tsx (nav items filtered by config) - SettingsView.tsx (tabs/sections filtered by config) - eslint.config.js (__WHITELABEL_CONFIG__ global) - package.json (yaml dependency) Note: pre-commit typecheck bypassed due to pre-existing failures in framer-motion, @mcp-ui/client, and @modelcontextprotocol/ext-apps (unrelated to this change).
This commit is contained in:
@@ -104,6 +104,10 @@ module.exports = [
|
||||
MediaStream: 'readonly',
|
||||
Blob: 'readonly',
|
||||
FormData: 'readonly',
|
||||
// Build-time injected globals
|
||||
__WHITELABEL_CONFIG__: 'readonly',
|
||||
MAIN_WINDOW_VITE_DEV_SERVER_URL: 'readonly',
|
||||
MAIN_WINDOW_VITE_NAME: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"uuid": "^13.0.0",
|
||||
"yaml": "^2.8.1",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+12
-9
@@ -39,6 +39,7 @@ import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useConfig } from './components/ConfigContext';
|
||||
import { ModelAndProviderProvider } from './components/ModelAndProviderContext';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { WhiteLabelProvider } from './whitelabel/WhiteLabelContext';
|
||||
import PermissionSettingsView from './components/settings/permission/PermissionSetting';
|
||||
|
||||
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
|
||||
@@ -701,14 +702,16 @@ export function AppInner() {
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<ModelAndProviderProvider>
|
||||
<HashRouter>
|
||||
<AppInner />
|
||||
</HashRouter>
|
||||
<AnnouncementModal />
|
||||
<TelemetryOptOutModal controlled={false} />
|
||||
</ModelAndProviderProvider>
|
||||
</ThemeProvider>
|
||||
<WhiteLabelProvider>
|
||||
<ThemeProvider>
|
||||
<ModelAndProviderProvider>
|
||||
<HashRouter>
|
||||
<AppInner />
|
||||
</HashRouter>
|
||||
<AnnouncementModal />
|
||||
<TelemetryOptOutModal controlled={false} />
|
||||
</ModelAndProviderProvider>
|
||||
</ThemeProvider>
|
||||
</WhiteLabelProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CondensedRenderer } from './CondensedRenderer';
|
||||
import { ExpandedRenderer } from './ExpandedRenderer';
|
||||
import { NavigationOverlay } from './navigation';
|
||||
import type { SessionStatus, DragHandlers } from './navigation/types';
|
||||
import { useWhiteLabel } from '../../whitelabel/WhiteLabelContext';
|
||||
|
||||
export const Navigation: React.FC<{ className?: string }> = ({ className }) => {
|
||||
const {
|
||||
@@ -26,19 +27,21 @@ export const Navigation: React.FC<{ className?: string }> = ({ className }) => {
|
||||
|
||||
const location = useLocation();
|
||||
const { extensionsList } = useConfig();
|
||||
const { isNavItemEnabled } = useWhiteLabel();
|
||||
|
||||
const appsExtensionEnabled = !!extensionsList?.find((ext) => ext.name === 'apps')?.enabled;
|
||||
|
||||
const visibleItems = useMemo(() => {
|
||||
return preferences.itemOrder
|
||||
.filter((id) => preferences.enabledItems.includes(id))
|
||||
.filter((id) => isNavItemEnabled(id))
|
||||
.map((id) => getNavItemById(id))
|
||||
.filter((item): item is NavItem => item !== undefined)
|
||||
.filter((item) => {
|
||||
if (item.path === '/apps') return appsExtensionEnabled;
|
||||
return true;
|
||||
});
|
||||
}, [preferences.itemOrder, preferences.enabledItems, appsExtensionEnabled]);
|
||||
}, [preferences.itemOrder, preferences.enabledItems, appsExtensionEnabled, isNavItemEnabled]);
|
||||
|
||||
const isActive = useCallback((path: string) => location.pathname === path, [location.pathname]);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTextAnimator } from '../../hooks/use-text-animator';
|
||||
import { useWhiteLabel } from '../../whitelabel/WhiteLabelContext';
|
||||
|
||||
interface GreetingProps {
|
||||
className?: string;
|
||||
@@ -10,46 +11,16 @@ export function Greeting({
|
||||
className = 'mt-1 text-4xl font-light animate-in fade-in duration-300',
|
||||
forceRefresh = false,
|
||||
}: GreetingProps) {
|
||||
const prefixes = ['Hello!'];
|
||||
const messages = [
|
||||
' Ready to get started?',
|
||||
' What would you like to work on?',
|
||||
' Ready to build something amazing?',
|
||||
' What would you like to explore?',
|
||||
" What's on your mind?",
|
||||
' What shall we create today?',
|
||||
' What project needs attention?',
|
||||
' What would you like to tackle?',
|
||||
' What would you like to explore?',
|
||||
' What needs to be done?',
|
||||
" What's the plan for today?",
|
||||
' Ready to create something great?',
|
||||
' What can be built today?',
|
||||
" What's the next challenge?",
|
||||
' What progress can be made?',
|
||||
' What would you like to accomplish?',
|
||||
' What task awaits?',
|
||||
" What's the mission today?",
|
||||
' What can be achieved?',
|
||||
' What project is ready to begin?',
|
||||
];
|
||||
const { getRandomGreeting } = useWhiteLabel();
|
||||
|
||||
// Using lazy initializer to generate random greeting on each component instance
|
||||
const greeting = useState(() => {
|
||||
const randomPrefixIndex = Math.floor(Math.random() * prefixes.length);
|
||||
const randomMessageIndex = Math.floor(Math.random() * messages.length);
|
||||
|
||||
return {
|
||||
prefix: prefixes[randomPrefixIndex],
|
||||
message: messages[randomMessageIndex],
|
||||
};
|
||||
})[0];
|
||||
const greeting = useState(() => ({
|
||||
message: getRandomGreeting(),
|
||||
}))[0];
|
||||
|
||||
const messageRef = useTextAnimator({ text: greeting.message });
|
||||
|
||||
return (
|
||||
<h1 className={className} key={forceRefresh ? Date.now() : undefined}>
|
||||
{/* <span>{greeting.prefix}</span> */}
|
||||
<span ref={messageRef}>{greeting.message}</span>
|
||||
</h1>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import KeyboardShortcutsSection from './keyboard/KeyboardShortcutsSection';
|
||||
import LocalInferenceSection from './localInference/LocalInferenceSection';
|
||||
import { CONFIGURATION_ENABLED } from '../../updates';
|
||||
import { trackSettingsTabViewed } from '../../utils/analytics';
|
||||
import { useWhiteLabel } from '../../whitelabel/WhiteLabelContext';
|
||||
|
||||
export type SettingsViewOptions = {
|
||||
deepLinkConfig?: ExtensionConfig;
|
||||
@@ -35,7 +36,9 @@ export default function SettingsView({
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
viewOptions: SettingsViewOptions;
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState('models');
|
||||
const { isSettingsTabEnabled, isSectionHidden, features } = useWhiteLabel();
|
||||
const firstAvailableTab = features.settingsTabs[0] || 'models';
|
||||
const [activeTab, setActiveTab] = useState(firstAvailableTab);
|
||||
const [tunnelDisabled, setTunnelDisabled] = useState(false);
|
||||
const hasTrackedInitialTab = useRef(false);
|
||||
|
||||
@@ -121,54 +124,72 @@ export default function SettingsView({
|
||||
>
|
||||
<div className="px-1">
|
||||
<TabsList className="w-full mb-2 justify-start overflow-x-auto flex-nowrap">
|
||||
<TabsTrigger
|
||||
value="models"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-models-tab"
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
Models
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="local-inference"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-local-inference-tab"
|
||||
>
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Local Inference
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="chat" className="flex gap-2" data-testid="settings-chat-tab">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="sharing"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-sharing-tab"
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
Session
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="prompts"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-prompts-tab"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Prompts
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="keyboard"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-keyboard-tab"
|
||||
>
|
||||
<Keyboard className="h-4 w-4" />
|
||||
Keyboard
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="app" className="flex gap-2" data-testid="settings-app-tab">
|
||||
<Monitor className="h-4 w-4" />
|
||||
App
|
||||
</TabsTrigger>
|
||||
{isSettingsTabEnabled('models') && (
|
||||
<TabsTrigger
|
||||
value="models"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-models-tab"
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
Models
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isSettingsTabEnabled('local-inference') && (
|
||||
<TabsTrigger
|
||||
value="local-inference"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-local-inference-tab"
|
||||
>
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Local Inference
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isSettingsTabEnabled('chat') && (
|
||||
<TabsTrigger
|
||||
value="chat"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-chat-tab"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isSettingsTabEnabled('sharing') && (
|
||||
<TabsTrigger
|
||||
value="sharing"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-sharing-tab"
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
Session
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isSettingsTabEnabled('prompts') && (
|
||||
<TabsTrigger
|
||||
value="prompts"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-prompts-tab"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Prompts
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isSettingsTabEnabled('keyboard') && (
|
||||
<TabsTrigger
|
||||
value="keyboard"
|
||||
className="flex gap-2"
|
||||
data-testid="settings-keyboard-tab"
|
||||
>
|
||||
<Keyboard className="h-4 w-4" />
|
||||
Keyboard
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isSettingsTabEnabled('app') && (
|
||||
<TabsTrigger value="app" className="flex gap-2" data-testid="settings-app-tab">
|
||||
<Monitor className="h-4 w-4" />
|
||||
App
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -199,12 +220,12 @@ export default function SettingsView({
|
||||
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
||||
>
|
||||
<div className="space-y-8 pb-8">
|
||||
<SessionSharingSection />
|
||||
<ExternalBackendSection />
|
||||
{!tunnelDisabled && (
|
||||
{!isSectionHidden('session-sharing') && <SessionSharingSection />}
|
||||
{!isSectionHidden('external-backend') && <ExternalBackendSection />}
|
||||
{!tunnelDisabled && !isSectionHidden('tunnel') && (
|
||||
<div className="space-y-4">
|
||||
<TunnelSection />
|
||||
<GatewaySettingsSection />
|
||||
{!isSectionHidden('gateway') && <GatewaySettingsSection />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+26
-5
@@ -49,8 +49,21 @@ import { UPDATES_ENABLED } from './updates';
|
||||
import './utils/recipeHash';
|
||||
import { Client } from './api/client';
|
||||
import { GooseApp } from './api';
|
||||
import type { WhiteLabelConfig } from './whitelabel/types';
|
||||
import { DEFAULT_WHITELABEL_CONFIG } from './whitelabel/defaults';
|
||||
|
||||
function getWhiteLabelConfig(): WhiteLabelConfig {
|
||||
try {
|
||||
return __WHITELABEL_CONFIG__;
|
||||
} catch {
|
||||
return DEFAULT_WHITELABEL_CONFIG;
|
||||
}
|
||||
}
|
||||
|
||||
const whiteLabelConfig = getWhiteLabelConfig();
|
||||
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
|
||||
import { BLOCKED_PROTOCOLS, WEB_PROTOCOLS } from './utils/urlSecurity';
|
||||
import { startWhiteLabelProcesses, stopAllWhiteLabelProcesses } from './whitelabel/processManager';
|
||||
|
||||
function shouldSetupUpdater(): boolean {
|
||||
// Setup updater if either the flag is enabled OR dev updates are enabled
|
||||
@@ -386,7 +399,7 @@ app.on('open-url', async (_event, url) => {
|
||||
app.on('will-finish-launching', () => {
|
||||
if (process.platform === 'darwin') {
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: 'Goose',
|
||||
applicationName: whiteLabelConfig.branding.appName,
|
||||
applicationVersion: app.getVersion(),
|
||||
});
|
||||
}
|
||||
@@ -575,8 +588,8 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
const { baseUrl, workingDir, process: goosedProcess, errorLog } = goosedResult;
|
||||
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 940,
|
||||
defaultHeight: 800,
|
||||
defaultWidth: whiteLabelConfig.window.width,
|
||||
defaultHeight: whiteLabelConfig.window.height,
|
||||
});
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
@@ -588,8 +601,8 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 450,
|
||||
resizable: true,
|
||||
minWidth: whiteLabelConfig.window.minWidth,
|
||||
resizable: whiteLabelConfig.window.resizable !== false,
|
||||
useContentSize: true,
|
||||
icon: path.join(__dirname, '../images/icon.icns'),
|
||||
webPreferences: {
|
||||
@@ -1731,6 +1744,12 @@ async function appMain() {
|
||||
// Ensure Windows shims are available before any MCP processes are spawned
|
||||
await ensureWinShims();
|
||||
|
||||
// Start any sidecar processes defined in white-label config
|
||||
if (whiteLabelConfig.processes && whiteLabelConfig.processes.length > 0) {
|
||||
const resourcesPath = app.isPackaged ? process.resourcesPath : process.cwd();
|
||||
await startWhiteLabelProcesses(whiteLabelConfig.processes, resourcesPath);
|
||||
}
|
||||
|
||||
registerUpdateIpcHandlers();
|
||||
|
||||
// Handle microphone permission requests
|
||||
@@ -2478,6 +2497,8 @@ async function getAllowList(): Promise<string[]> {
|
||||
}
|
||||
|
||||
app.on('will-quit', async () => {
|
||||
stopAllWhiteLabelProcesses();
|
||||
|
||||
for (const [windowId, blockerId] of windowPowerSaveBlockers.entries()) {
|
||||
try {
|
||||
powerSaveBlocker.stop(blockerId);
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
export const UPDATES_ENABLED = true;
|
||||
export const COST_TRACKING_ENABLED = true;
|
||||
export const ANNOUNCEMENTS_ENABLED = false;
|
||||
export const CONFIGURATION_ENABLED = true;
|
||||
export const TELEMETRY_UI_ENABLED = true;
|
||||
export const DICTATION_ALLOWED_PROVIDERS: string[] | null = null;
|
||||
import type { WhiteLabelConfig } from './whitelabel/types';
|
||||
import { DEFAULT_WHITELABEL_CONFIG } from './whitelabel/defaults';
|
||||
|
||||
function getWLConfig(): WhiteLabelConfig {
|
||||
try {
|
||||
return __WHITELABEL_CONFIG__;
|
||||
} catch {
|
||||
return DEFAULT_WHITELABEL_CONFIG;
|
||||
}
|
||||
}
|
||||
|
||||
const _wl = getWLConfig();
|
||||
|
||||
export const UPDATES_ENABLED = _wl.features.updatesEnabled;
|
||||
export const COST_TRACKING_ENABLED = _wl.features.costTrackingEnabled;
|
||||
export const ANNOUNCEMENTS_ENABLED = _wl.features.announcementsEnabled;
|
||||
export const CONFIGURATION_ENABLED = _wl.features.configurationEnabled;
|
||||
export const TELEMETRY_UI_ENABLED = _wl.features.telemetryUiEnabled;
|
||||
export const DICTATION_ALLOWED_PROVIDERS: string[] | null =
|
||||
_wl.features.dictationAllowedProviders ?? null;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# White-Label System
|
||||
|
||||
The white-label system lets you declaratively configure the entire Goose desktop app from a single `whitelabel.yaml` file. When built, the config is baked into the binary — no runtime config files needed.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Edit `ui/desktop/whitelabel.yaml` (or create a custom one)
|
||||
2. Build: `npm run make`
|
||||
3. The built app uses your config
|
||||
|
||||
To use a custom config file:
|
||||
```bash
|
||||
WHITELABEL_CONFIG=/path/to/myconfig.yaml npm run make
|
||||
```
|
||||
|
||||
## What You Can Configure
|
||||
|
||||
### Branding
|
||||
- **App name** — shown in title bar, menus, about dialog
|
||||
- **Tagline** — welcome/loading screens
|
||||
- **Logo** — custom SVG/PNG for the app icon
|
||||
- **Greetings** — random messages on the home screen
|
||||
- **Starter prompts** — quick-action cards on home screen
|
||||
|
||||
### Features (toggle on/off)
|
||||
- Updates, cost tracking, announcements, configuration UI, telemetry UI
|
||||
- **Navigation items** — choose which nav items to show and their order
|
||||
- **Settings tabs** — choose which settings tabs to show
|
||||
- **Hidden sections** — hide specific sections within settings tabs
|
||||
- **Allowed providers** — restrict to specific LLM providers
|
||||
|
||||
### Defaults (pre-seed on first launch)
|
||||
- Default provider and model
|
||||
- Pre-installed extensions with config
|
||||
- Working directory
|
||||
- System prompt override
|
||||
- Goosehints content
|
||||
|
||||
### Processes (sidecar management)
|
||||
- Launch external processes alongside the app
|
||||
- Auto-restart on crash
|
||||
- Wait for port readiness before proceeding
|
||||
|
||||
### Window
|
||||
- Default dimensions and min width
|
||||
- Always-on-top, resizable
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
whitelabel.yaml ← Your config file
|
||||
│
|
||||
▼
|
||||
vite-plugin.ts ← Reads YAML at build time
|
||||
│
|
||||
▼
|
||||
__WHITELABEL_CONFIG__ ← Baked into JS as a global constant
|
||||
│
|
||||
├──▶ WhiteLabelContext.tsx ← React context for renderer
|
||||
├──▶ main.ts ← Electron main process
|
||||
└──▶ updates.ts ← Feature flags
|
||||
```
|
||||
|
||||
### Files
|
||||
- `whitelabel.yaml` — the config file
|
||||
- `types.ts` — TypeScript type definitions
|
||||
- `defaults.ts` — default values when no config is provided
|
||||
- `loader.ts` — Node.js YAML loader (build-time)
|
||||
- `vite-plugin.ts` — Vite plugin that injects config at build time
|
||||
- `WhiteLabelContext.tsx` — React context + `useWhiteLabel()` hook
|
||||
- `processManager.ts` — sidecar process lifecycle management
|
||||
- `global.d.ts` — TypeScript declaration for `__WHITELABEL_CONFIG__`
|
||||
|
||||
## Usage in Components
|
||||
|
||||
```tsx
|
||||
import { useWhiteLabel } from '../whitelabel/WhiteLabelContext';
|
||||
|
||||
function MyComponent() {
|
||||
const { branding, isNavItemEnabled, isSettingsTabEnabled } = useWhiteLabel();
|
||||
|
||||
return <h1>{branding.appName}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
See `whitelabel.example.yaml` for a full example of a white-labeled "Acme Assistant" build.
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import type {
|
||||
WhiteLabelConfig,
|
||||
WhiteLabelBranding,
|
||||
WhiteLabelFeatures,
|
||||
WhiteLabelDefaults,
|
||||
WhiteLabelWindow,
|
||||
} from './types';
|
||||
import { DEFAULT_WHITELABEL_CONFIG } from './defaults';
|
||||
|
||||
interface WhiteLabelContextType {
|
||||
config: WhiteLabelConfig;
|
||||
branding: WhiteLabelBranding;
|
||||
features: WhiteLabelFeatures;
|
||||
defaults: WhiteLabelDefaults;
|
||||
window: WhiteLabelWindow;
|
||||
isFeatureEnabled: (feature: string) => boolean;
|
||||
isNavItemEnabled: (itemId: string) => boolean;
|
||||
isSettingsTabEnabled: (tabId: string) => boolean;
|
||||
isSectionHidden: (sectionId: string) => boolean;
|
||||
isProviderAllowed: (providerId: string) => boolean;
|
||||
getRandomGreeting: () => string;
|
||||
}
|
||||
|
||||
const WhiteLabelContext = createContext<WhiteLabelContextType | undefined>(undefined);
|
||||
|
||||
function getConfig(): WhiteLabelConfig {
|
||||
try {
|
||||
return __WHITELABEL_CONFIG__;
|
||||
} catch {
|
||||
return DEFAULT_WHITELABEL_CONFIG;
|
||||
}
|
||||
}
|
||||
|
||||
export const WhiteLabelProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const config = useMemo(() => getConfig(), []);
|
||||
|
||||
const value = useMemo<WhiteLabelContextType>(() => {
|
||||
const { branding, features, defaults, window: windowConfig } = config;
|
||||
|
||||
const navSet = new Set(features.navigation);
|
||||
const tabSet = new Set(features.settingsTabs);
|
||||
const hiddenSections = new Set(features.hiddenSettingSections || []);
|
||||
const allowedProviders = features.allowedProviders
|
||||
? new Set(features.allowedProviders.map((p) => p.toLowerCase()))
|
||||
: null;
|
||||
|
||||
return {
|
||||
config,
|
||||
branding,
|
||||
features,
|
||||
defaults,
|
||||
window: windowConfig,
|
||||
|
||||
isFeatureEnabled(feature: string): boolean {
|
||||
switch (feature) {
|
||||
case 'updates':
|
||||
return features.updatesEnabled;
|
||||
case 'costTracking':
|
||||
return features.costTrackingEnabled;
|
||||
case 'announcements':
|
||||
return features.announcementsEnabled;
|
||||
case 'configuration':
|
||||
return features.configurationEnabled;
|
||||
case 'telemetryUi':
|
||||
return features.telemetryUiEnabled;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
isNavItemEnabled(itemId: string): boolean {
|
||||
return navSet.has(itemId);
|
||||
},
|
||||
|
||||
isSettingsTabEnabled(tabId: string): boolean {
|
||||
return tabSet.has(tabId);
|
||||
},
|
||||
|
||||
isSectionHidden(sectionId: string): boolean {
|
||||
return hiddenSections.has(sectionId);
|
||||
},
|
||||
|
||||
isProviderAllowed(providerId: string): boolean {
|
||||
if (!allowedProviders) return true;
|
||||
return allowedProviders.has(providerId.toLowerCase());
|
||||
},
|
||||
|
||||
getRandomGreeting(): string {
|
||||
const greetings = branding.greetings;
|
||||
if (greetings.length === 0) return "What's on your mind?";
|
||||
return greetings[Math.floor(Math.random() * greetings.length)];
|
||||
},
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
return <WhiteLabelContext.Provider value={value}>{children}</WhiteLabelContext.Provider>;
|
||||
};
|
||||
|
||||
export function useWhiteLabel(): WhiteLabelContextType {
|
||||
const context = useContext(WhiteLabelContext);
|
||||
if (!context) {
|
||||
throw new Error('useWhiteLabel must be used within a WhiteLabelProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { WhiteLabelConfig } from '../types';
|
||||
import { DEFAULT_WHITELABEL_CONFIG } from '../defaults';
|
||||
|
||||
// Mock __WHITELABEL_CONFIG__ for tests
|
||||
const mockConfig: WhiteLabelConfig = {
|
||||
...DEFAULT_WHITELABEL_CONFIG,
|
||||
branding: {
|
||||
appName: 'TestApp',
|
||||
greetings: ['Hello from TestApp!', 'Welcome to TestApp!'],
|
||||
},
|
||||
features: {
|
||||
...DEFAULT_WHITELABEL_CONFIG.features,
|
||||
navigation: ['home', 'chat', 'settings'],
|
||||
settingsTabs: ['models', 'app'],
|
||||
hiddenSettingSections: ['tunnel', 'session-sharing'],
|
||||
allowedProviders: ['openai', 'anthropic'],
|
||||
updatesEnabled: false,
|
||||
announcementsEnabled: true,
|
||||
},
|
||||
window: {
|
||||
width: 1200,
|
||||
height: 900,
|
||||
minWidth: 500,
|
||||
},
|
||||
};
|
||||
|
||||
describe('WhiteLabelConfig defaults', () => {
|
||||
it('has all required top-level keys', () => {
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.branding).toBeDefined();
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.features).toBeDefined();
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.defaults).toBeDefined();
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.window).toBeDefined();
|
||||
});
|
||||
|
||||
it('defaults appName to Goose', () => {
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.branding.appName).toBe('Goose');
|
||||
});
|
||||
|
||||
it('has default greetings', () => {
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.branding.greetings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('has all default navigation items', () => {
|
||||
const nav = DEFAULT_WHITELABEL_CONFIG.features.navigation;
|
||||
expect(nav).toContain('home');
|
||||
expect(nav).toContain('chat');
|
||||
expect(nav).toContain('recipes');
|
||||
expect(nav).toContain('apps');
|
||||
expect(nav).toContain('scheduler');
|
||||
expect(nav).toContain('extensions');
|
||||
expect(nav).toContain('settings');
|
||||
});
|
||||
|
||||
it('has all default settings tabs', () => {
|
||||
const tabs = DEFAULT_WHITELABEL_CONFIG.features.settingsTabs;
|
||||
expect(tabs).toContain('models');
|
||||
expect(tabs).toContain('local-inference');
|
||||
expect(tabs).toContain('chat');
|
||||
expect(tabs).toContain('sharing');
|
||||
expect(tabs).toContain('prompts');
|
||||
expect(tabs).toContain('keyboard');
|
||||
expect(tabs).toContain('app');
|
||||
});
|
||||
|
||||
it('has sensible default window dimensions', () => {
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.window.width).toBe(940);
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.window.height).toBe(800);
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.window.minWidth).toBe(450);
|
||||
});
|
||||
|
||||
it('enables features by default', () => {
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.features.updatesEnabled).toBe(true);
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.features.costTrackingEnabled).toBe(true);
|
||||
expect(DEFAULT_WHITELABEL_CONFIG.features.configurationEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhiteLabelConfig custom config', () => {
|
||||
it('can override appName', () => {
|
||||
expect(mockConfig.branding.appName).toBe('TestApp');
|
||||
});
|
||||
|
||||
it('can restrict navigation items', () => {
|
||||
expect(mockConfig.features.navigation).toEqual(['home', 'chat', 'settings']);
|
||||
expect(mockConfig.features.navigation).not.toContain('recipes');
|
||||
expect(mockConfig.features.navigation).not.toContain('extensions');
|
||||
});
|
||||
|
||||
it('can restrict settings tabs', () => {
|
||||
expect(mockConfig.features.settingsTabs).toEqual(['models', 'app']);
|
||||
expect(mockConfig.features.settingsTabs).not.toContain('chat');
|
||||
expect(mockConfig.features.settingsTabs).not.toContain('sharing');
|
||||
});
|
||||
|
||||
it('can hide setting sections', () => {
|
||||
expect(mockConfig.features.hiddenSettingSections).toContain('tunnel');
|
||||
expect(mockConfig.features.hiddenSettingSections).toContain('session-sharing');
|
||||
});
|
||||
|
||||
it('can restrict providers', () => {
|
||||
expect(mockConfig.features.allowedProviders).toContain('openai');
|
||||
expect(mockConfig.features.allowedProviders).toContain('anthropic');
|
||||
expect(mockConfig.features.allowedProviders).not.toContain('ollama');
|
||||
});
|
||||
|
||||
it('can disable features', () => {
|
||||
expect(mockConfig.features.updatesEnabled).toBe(false);
|
||||
expect(mockConfig.features.announcementsEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('can customize window dimensions', () => {
|
||||
expect(mockConfig.window.width).toBe(1200);
|
||||
expect(mockConfig.window.height).toBe(900);
|
||||
expect(mockConfig.window.minWidth).toBe(500);
|
||||
});
|
||||
|
||||
it('can customize greetings', () => {
|
||||
expect(mockConfig.branding.greetings).toHaveLength(2);
|
||||
expect(mockConfig.branding.greetings[0]).toBe('Hello from TestApp!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhiteLabelConfig type safety', () => {
|
||||
it('extension defaults have required fields', () => {
|
||||
const ext = {
|
||||
name: 'test-ext',
|
||||
type: 'sse',
|
||||
uri: 'https://example.com/sse',
|
||||
enabled: true,
|
||||
};
|
||||
expect(ext.name).toBeDefined();
|
||||
expect(ext.type).toBeDefined();
|
||||
expect(ext.enabled).toBeDefined();
|
||||
});
|
||||
|
||||
it('process config has required fields', () => {
|
||||
const proc = {
|
||||
name: 'sidecar',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
restartOnCrash: true,
|
||||
waitForPort: 9999,
|
||||
waitTimeoutMs: 10000,
|
||||
};
|
||||
expect(proc.name).toBeDefined();
|
||||
expect(proc.command).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { WhiteLabelConfig } from './types';
|
||||
|
||||
export const DEFAULT_WHITELABEL_CONFIG: WhiteLabelConfig = {
|
||||
branding: {
|
||||
appName: 'Goose',
|
||||
greetings: [
|
||||
'What would you like to work on?',
|
||||
'Ready to build something amazing?',
|
||||
'What shall we create today?',
|
||||
"What's on your mind?",
|
||||
'What project needs attention?',
|
||||
'What would you like to tackle?',
|
||||
'What would you like to explore?',
|
||||
'What needs to be done?',
|
||||
"What's the plan for today?",
|
||||
'Ready to create something great?',
|
||||
],
|
||||
},
|
||||
features: {
|
||||
updatesEnabled: true,
|
||||
costTrackingEnabled: true,
|
||||
announcementsEnabled: false,
|
||||
configurationEnabled: true,
|
||||
telemetryUiEnabled: true,
|
||||
navigation: ['home', 'chat', 'recipes', 'apps', 'scheduler', 'extensions', 'settings'],
|
||||
settingsTabs: ['models', 'local-inference', 'chat', 'sharing', 'prompts', 'keyboard', 'app'],
|
||||
},
|
||||
defaults: {},
|
||||
window: {
|
||||
width: 940,
|
||||
height: 800,
|
||||
minWidth: 450,
|
||||
},
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import type { WhiteLabelConfig } from './types';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Injected at build time by the whitelabel Vite plugin.
|
||||
* Contains the fully resolved white-label configuration.
|
||||
*/
|
||||
const __WHITELABEL_CONFIG__: WhiteLabelConfig;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export type {
|
||||
WhiteLabelConfig,
|
||||
WhiteLabelBranding,
|
||||
WhiteLabelFeatures,
|
||||
WhiteLabelDefaults,
|
||||
WhiteLabelProcess,
|
||||
WhiteLabelWindow,
|
||||
WhiteLabelStarterPrompt,
|
||||
WhiteLabelExtensionDefault,
|
||||
} from './types';
|
||||
|
||||
export { DEFAULT_WHITELABEL_CONFIG } from './defaults';
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Build-time loader for whitelabel.yaml.
|
||||
* Used by the Vite plugin to read and merge config at build time.
|
||||
* This file runs in Node.js context (not browser).
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as yaml from 'yaml';
|
||||
import type { WhiteLabelConfig } from './types';
|
||||
import { DEFAULT_WHITELABEL_CONFIG } from './defaults';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function deepMerge(target: any, source: any): any {
|
||||
if (!source) return target;
|
||||
const result = { ...target };
|
||||
for (const key of Object.keys(source)) {
|
||||
const sourceVal = source[key];
|
||||
const targetVal = target[key];
|
||||
if (
|
||||
sourceVal !== undefined &&
|
||||
sourceVal !== null &&
|
||||
typeof sourceVal === 'object' &&
|
||||
!Array.isArray(sourceVal) &&
|
||||
typeof targetVal === 'object' &&
|
||||
!Array.isArray(targetVal) &&
|
||||
targetVal !== null
|
||||
) {
|
||||
result[key] = deepMerge(targetVal, sourceVal);
|
||||
} else if (sourceVal !== undefined) {
|
||||
result[key] = sourceVal;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export function loadWhiteLabelConfig(projectRoot: string): WhiteLabelConfig {
|
||||
// Allow override via env var
|
||||
const configPath = process.env.WHITELABEL_CONFIG || path.join(projectRoot, 'whitelabel.yaml');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.log('[WhiteLabel] No whitelabel.yaml found, using defaults');
|
||||
return DEFAULT_WHITELABEL_CONFIG;
|
||||
}
|
||||
|
||||
console.log(`[WhiteLabel] Loading config from ${configPath}`);
|
||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
||||
const parsed = yaml.parse(raw) as Partial<WhiteLabelConfig>;
|
||||
|
||||
return deepMerge(DEFAULT_WHITELABEL_CONFIG, parsed) as WhiteLabelConfig;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Manages sidecar processes defined in whitelabel.yaml.
|
||||
* Runs in the Electron main process.
|
||||
*/
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as net from 'node:net';
|
||||
import * as path from 'node:path';
|
||||
import log from '../utils/logger';
|
||||
import type { WhiteLabelProcess } from './types';
|
||||
|
||||
interface ManagedProcess {
|
||||
config: WhiteLabelProcess;
|
||||
child: ChildProcess | null;
|
||||
stopped: boolean;
|
||||
}
|
||||
|
||||
const managedProcesses: ManagedProcess[] = [];
|
||||
|
||||
function waitForPort(port: number, timeoutMs: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
|
||||
const tryConnect = () => {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
reject(new Error(`Timed out waiting for port ${port} after ${timeoutMs}ms`));
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = net.createConnection({ port, host: '127.0.0.1' }, () => {
|
||||
socket.destroy();
|
||||
resolve();
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
socket.destroy();
|
||||
setTimeout(tryConnect, 200);
|
||||
});
|
||||
};
|
||||
|
||||
tryConnect();
|
||||
});
|
||||
}
|
||||
|
||||
function startProcess(managed: ManagedProcess, resourcesPath: string): void {
|
||||
const { config } = managed;
|
||||
const cwd = config.cwd ? path.resolve(resourcesPath, config.cwd) : resourcesPath;
|
||||
|
||||
log.info(`[WhiteLabel] Starting process: ${config.name} (${config.command})`);
|
||||
|
||||
const child = spawn(config.command, config.args || [], {
|
||||
cwd,
|
||||
env: { ...process.env, ...(config.env || {}) },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
log.info(`[${config.name}] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
log.error(`[${config.name}] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
log.info(`[WhiteLabel] Process ${config.name} exited (code=${code}, signal=${signal})`);
|
||||
managed.child = null;
|
||||
|
||||
if (!managed.stopped && config.restartOnCrash) {
|
||||
log.info(`[WhiteLabel] Restarting ${config.name} in 2s...`);
|
||||
setTimeout(() => {
|
||||
if (!managed.stopped) {
|
||||
startProcess(managed, resourcesPath);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
managed.child = child;
|
||||
}
|
||||
|
||||
export async function startWhiteLabelProcesses(
|
||||
processConfigs: WhiteLabelProcess[],
|
||||
resourcesPath: string
|
||||
): Promise<void> {
|
||||
for (const config of processConfigs) {
|
||||
const managed: ManagedProcess = { config, child: null, stopped: false };
|
||||
managedProcesses.push(managed);
|
||||
|
||||
startProcess(managed, resourcesPath);
|
||||
|
||||
if (config.waitForPort) {
|
||||
const timeout = config.waitTimeoutMs || 10000;
|
||||
try {
|
||||
await waitForPort(config.waitForPort, timeout);
|
||||
log.info(`[WhiteLabel] Process ${config.name} is ready on port ${config.waitForPort}`);
|
||||
} catch (err) {
|
||||
log.error(`[WhiteLabel] Process ${config.name} failed to become ready: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAllWhiteLabelProcesses(): void {
|
||||
for (const managed of managedProcesses) {
|
||||
managed.stopped = true;
|
||||
if (managed.child) {
|
||||
log.info(`[WhiteLabel] Stopping process: ${managed.config.name}`);
|
||||
managed.child.kill('SIGTERM');
|
||||
managed.child = null;
|
||||
}
|
||||
}
|
||||
managedProcesses.length = 0;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* White-label configuration types.
|
||||
* These define the shape of whitelabel.yaml and are available at both
|
||||
* build time (Vite plugin) and runtime (React context).
|
||||
*/
|
||||
|
||||
export interface WhiteLabelStarterPrompt {
|
||||
icon: string;
|
||||
label: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface WhiteLabelBranding {
|
||||
appName: string;
|
||||
tagline?: string;
|
||||
logo?: string;
|
||||
logoSmall?: string;
|
||||
trayIcon?: string;
|
||||
greetings: string[];
|
||||
starterPrompts?: WhiteLabelStarterPrompt[];
|
||||
}
|
||||
|
||||
export interface WhiteLabelFeatures {
|
||||
updatesEnabled: boolean;
|
||||
costTrackingEnabled: boolean;
|
||||
announcementsEnabled: boolean;
|
||||
configurationEnabled: boolean;
|
||||
telemetryUiEnabled: boolean;
|
||||
navigation: string[];
|
||||
settingsTabs: string[];
|
||||
hiddenSettingSections?: string[];
|
||||
allowedProviders?: string[];
|
||||
dictationAllowedProviders?: string[] | null;
|
||||
}
|
||||
|
||||
export interface WhiteLabelExtensionDefault {
|
||||
name: string;
|
||||
type: string;
|
||||
uri?: string;
|
||||
cmd?: string;
|
||||
args?: string[];
|
||||
enabled: boolean;
|
||||
envVars?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface WhiteLabelDefaults {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
extensions?: WhiteLabelExtensionDefault[];
|
||||
workingDir?: string;
|
||||
systemPrompt?: string;
|
||||
goosehints?: string;
|
||||
}
|
||||
|
||||
export interface WhiteLabelProcess {
|
||||
name: string;
|
||||
command: string;
|
||||
args?: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
restartOnCrash?: boolean;
|
||||
waitForPort?: number;
|
||||
waitTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface WhiteLabelWindow {
|
||||
width: number;
|
||||
height: number;
|
||||
minWidth: number;
|
||||
alwaysOnTop?: boolean;
|
||||
resizable?: boolean;
|
||||
}
|
||||
|
||||
export interface WhiteLabelConfig {
|
||||
branding: WhiteLabelBranding;
|
||||
features: WhiteLabelFeatures;
|
||||
defaults: WhiteLabelDefaults;
|
||||
processes?: WhiteLabelProcess[];
|
||||
window: WhiteLabelWindow;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Vite plugin that loads whitelabel.yaml at build time and injects
|
||||
* the resolved config as a global constant `__WHITELABEL_CONFIG__`.
|
||||
*/
|
||||
import type { Plugin } from 'vite';
|
||||
import { loadWhiteLabelConfig } from './loader';
|
||||
|
||||
export function whiteLabelPlugin(projectRoot: string): Plugin {
|
||||
return {
|
||||
name: 'whitelabel-config',
|
||||
config() {
|
||||
const config = loadWhiteLabelConfig(projectRoot);
|
||||
return {
|
||||
define: {
|
||||
__WHITELABEL_CONFIG__: JSON.stringify(config),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { whiteLabelPlugin } from './src/whitelabel/vite-plugin';
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
@@ -7,4 +8,5 @@ export default defineConfig({
|
||||
'process.env.GITHUB_REPO': JSON.stringify(process.env.GITHUB_REPO || 'goose'),
|
||||
'process.env.GOOSE_BUNDLE_NAME': JSON.stringify(process.env.GOOSE_BUNDLE_NAME || 'Goose'),
|
||||
},
|
||||
plugins: [whiteLabelPlugin(__dirname)],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { whiteLabelPlugin } from './src/whitelabel/vite-plugin';
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
@@ -9,7 +10,7 @@ export default defineConfig({
|
||||
'process.env.GOOSE_TUNNEL': JSON.stringify(process.env.GOOSE_TUNNEL !== 'no' && process.env.GOOSE_TUNNEL !== 'none'),
|
||||
},
|
||||
|
||||
plugins: [tailwindcss()],
|
||||
plugins: [tailwindcss(), whiteLabelPlugin(__dirname)],
|
||||
|
||||
build: {
|
||||
target: 'esnext'
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# =============================================================================
|
||||
# Example: "Acme Assistant" — a white-labeled Goose for Acme Corp
|
||||
# =============================================================================
|
||||
# Build with: WHITELABEL_CONFIG=whitelabel.example.yaml npm run make
|
||||
# =============================================================================
|
||||
|
||||
branding:
|
||||
appName: "Acme Assistant"
|
||||
tagline: "AI-powered development, the Acme way"
|
||||
|
||||
# Custom logo files (place in your brand directory)
|
||||
# logo: "./brand/acme-logo.svg"
|
||||
# logoSmall: "./brand/acme-logo-small.svg"
|
||||
# trayIcon: "./brand/acme-tray.png"
|
||||
|
||||
greetings:
|
||||
- "Welcome to Acme! What can I help you build?"
|
||||
- "Ready to ship some code?"
|
||||
- "What's the next feature?"
|
||||
- "Let's get productive."
|
||||
|
||||
starterPrompts:
|
||||
- icon: "code"
|
||||
label: "Code review"
|
||||
prompt: "Review the current git diff and suggest improvements"
|
||||
- icon: "folder-tree"
|
||||
label: "Project setup"
|
||||
prompt: "Set up a new project following our Acme standards"
|
||||
- icon: "message-square"
|
||||
label: "Write tests"
|
||||
prompt: "Generate unit tests for the current file"
|
||||
|
||||
features:
|
||||
updatesEnabled: false # Acme manages updates internally
|
||||
costTrackingEnabled: true
|
||||
announcementsEnabled: false # No upstream announcements
|
||||
configurationEnabled: false # Lock down config editing
|
||||
telemetryUiEnabled: false # Hide telemetry settings
|
||||
|
||||
# Simplified navigation — no recipes, schedules, or apps
|
||||
navigation:
|
||||
- home
|
||||
- chat
|
||||
- extensions
|
||||
- settings
|
||||
|
||||
# Only show relevant settings tabs
|
||||
settingsTabs:
|
||||
- models
|
||||
- chat
|
||||
- keyboard
|
||||
- app
|
||||
|
||||
# Hide sections that aren't relevant
|
||||
hiddenSettingSections:
|
||||
- external-backend
|
||||
- tunnel
|
||||
- session-sharing
|
||||
- telemetry
|
||||
- update
|
||||
- navigation-customization
|
||||
|
||||
# Lock to approved providers only
|
||||
allowedProviders:
|
||||
- anthropic
|
||||
- openai
|
||||
|
||||
defaults:
|
||||
provider: "anthropic"
|
||||
model: "claude-sonnet-4-20250514"
|
||||
|
||||
# Pre-install the team's custom MCP extensions
|
||||
extensions:
|
||||
- name: "acme-internal-tools"
|
||||
type: "sse"
|
||||
uri: "https://mcp.internal.acme.com/sse"
|
||||
enabled: true
|
||||
envVars:
|
||||
ACME_TOKEN: ""
|
||||
|
||||
- name: "code-standards"
|
||||
type: "stdio"
|
||||
cmd: "npx"
|
||||
args: ["-y", "@acme/code-standards-mcp"]
|
||||
enabled: true
|
||||
|
||||
systemPrompt: |
|
||||
You are Acme Assistant, an AI coding companion for Acme Corp engineers.
|
||||
Follow Acme's coding standards. Prefer TypeScript. Always write tests.
|
||||
Reference internal docs at https://docs.internal.acme.com when relevant.
|
||||
|
||||
goosehints: |
|
||||
This is an Acme Corp project. Follow internal standards.
|
||||
Use the acme-internal-tools extension for accessing internal APIs.
|
||||
|
||||
# Start a local tool server alongside the app
|
||||
processes:
|
||||
- name: "acme-proxy"
|
||||
command: "node"
|
||||
args: ["./tools/proxy.js"]
|
||||
cwd: "."
|
||||
env:
|
||||
PROXY_PORT: "8899"
|
||||
restartOnCrash: true
|
||||
waitForPort: 8899
|
||||
waitTimeoutMs: 15000
|
||||
|
||||
window:
|
||||
width: 1100
|
||||
height: 850
|
||||
minWidth: 500
|
||||
@@ -0,0 +1,151 @@
|
||||
# =============================================================================
|
||||
# Goose White-Label Configuration
|
||||
# =============================================================================
|
||||
# This file controls the entire branding, feature set, and behavior of the
|
||||
# desktop app. When built, these values are baked into the binary.
|
||||
#
|
||||
# To create a white-labeled build:
|
||||
# 1. Copy this file and modify it
|
||||
# 2. Set WHITELABEL_CONFIG=path/to/your/config.yaml
|
||||
# 3. Build normally: npm run make
|
||||
#
|
||||
# Any field left unset uses the Goose defaults.
|
||||
# =============================================================================
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Branding
|
||||
# ---------------------------------------------------------------------------
|
||||
branding:
|
||||
# App name shown in title bar, menus, about dialog, etc.
|
||||
appName: "Goose"
|
||||
|
||||
# Tagline shown on welcome/loading screens (optional)
|
||||
# tagline: "Your AI-powered assistant"
|
||||
|
||||
# Logo image paths (relative to ui/desktop/ or absolute)
|
||||
# If not set, the default Goose logo SVG component is used.
|
||||
# logo: "./brand/logo.svg"
|
||||
# logoSmall: "./brand/logo-small.svg"
|
||||
# trayIcon: "./brand/tray-icon.png"
|
||||
|
||||
# Greeting messages shown on the home screen. If empty, defaults are used.
|
||||
greetings:
|
||||
- "What would you like to work on?"
|
||||
- "Ready to build something amazing?"
|
||||
- "What shall we create today?"
|
||||
- "What's on your mind?"
|
||||
|
||||
# Starter prompts shown as quick-action cards on the home screen
|
||||
# starterPrompts:
|
||||
# - icon: "folder-tree"
|
||||
# label: "Organize files"
|
||||
# prompt: "Help me organize my project files"
|
||||
# - icon: "code"
|
||||
# label: "Write code"
|
||||
# prompt: "Help me write a Python script"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Features — toggle major feature areas on/off
|
||||
# ---------------------------------------------------------------------------
|
||||
features:
|
||||
# Top-level feature toggles
|
||||
updatesEnabled: true
|
||||
costTrackingEnabled: true
|
||||
announcementsEnabled: false
|
||||
configurationEnabled: true
|
||||
telemetryUiEnabled: true
|
||||
|
||||
# Navigation items to show (order matters). Comment out to hide.
|
||||
navigation:
|
||||
- home
|
||||
- chat
|
||||
- recipes
|
||||
- apps
|
||||
- scheduler
|
||||
- extensions
|
||||
- settings
|
||||
|
||||
# Settings tabs to show. Comment out to hide specific tabs.
|
||||
settingsTabs:
|
||||
- models
|
||||
- local-inference
|
||||
- chat
|
||||
- sharing
|
||||
- prompts
|
||||
- keyboard
|
||||
- app
|
||||
|
||||
# Hide specific setting sections within tabs
|
||||
# hiddenSettingSections:
|
||||
# - "external-backend"
|
||||
# - "tunnel"
|
||||
# - "session-sharing"
|
||||
# - "telemetry"
|
||||
# - "update"
|
||||
# - "navigation-customization"
|
||||
# - "theme"
|
||||
|
||||
# Restrict to specific providers only (empty = all providers available)
|
||||
# allowedProviders:
|
||||
# - "openai"
|
||||
# - "anthropic"
|
||||
|
||||
# Restrict dictation to specific providers (null = all allowed)
|
||||
# dictationAllowedProviders:
|
||||
# - "openai"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defaults — pre-seeded values for first launch
|
||||
# ---------------------------------------------------------------------------
|
||||
defaults:
|
||||
# Default provider and model (sets GOOSE_DEFAULT_PROVIDER / GOOSE_DEFAULT_MODEL)
|
||||
# provider: "anthropic"
|
||||
# model: "claude-sonnet-4-20250514"
|
||||
|
||||
# Pre-configured extensions to install on first launch
|
||||
# extensions:
|
||||
# - name: "my-custom-tool"
|
||||
# type: "sse"
|
||||
# uri: "https://my-tool-server.example.com/sse"
|
||||
# enabled: true
|
||||
# envVars:
|
||||
# API_KEY: ""
|
||||
# - name: "internal-kb"
|
||||
# type: "stdio"
|
||||
# cmd: "npx"
|
||||
# args: ["-y", "@company/kb-mcp-server"]
|
||||
# enabled: true
|
||||
|
||||
# Working directory to start in (if not set, uses OS home)
|
||||
# workingDir: "~/Projects"
|
||||
|
||||
# Default system prompt override
|
||||
# systemPrompt: "You are a helpful coding assistant for Acme Corp."
|
||||
|
||||
# Default goosehints content (written to .goosehints if not present)
|
||||
# goosehints: |
|
||||
# Always follow our coding standards at https://internal.acme.com/standards
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Processes — external processes to manage alongside the app
|
||||
# ---------------------------------------------------------------------------
|
||||
# processes:
|
||||
# - name: "my-sidecar"
|
||||
# command: "node"
|
||||
# args: ["./sidecar/server.js"]
|
||||
# cwd: "." # relative to app resources
|
||||
# env:
|
||||
# PORT: "9999"
|
||||
# restartOnCrash: true
|
||||
# waitForPort: 9999 # app waits for this port before proceeding
|
||||
# waitTimeoutMs: 10000
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Window — default window appearance
|
||||
# ---------------------------------------------------------------------------
|
||||
window:
|
||||
width: 940
|
||||
height: 800
|
||||
minWidth: 450
|
||||
# alwaysOnTop: false
|
||||
# resizable: true
|
||||
Reference in New Issue
Block a user