Skip to content

Commit

Permalink
fix: deep link installs of extensions (#1333)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexhancock authored Feb 21, 2025
1 parent 78f4500 commit ffe020e
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 83 deletions.
18 changes: 16 additions & 2 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import { extractExtensionName } from './components/settings/extensions/utils';

import WelcomeView from './components/WelcomeView';
import ChatView from './components/ChatView';
import SettingsView from './components/settings/SettingsView';
import SettingsView, { type SettingsViewOptions } from './components/settings/SettingsView';
import MoreModelsView from './components/settings/models/MoreModelsView';
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';

import 'react-toastify/dist/ReactToastify.css';

// Views and their options
export type View =
| 'welcome'
| 'chat'
Expand All @@ -27,15 +28,27 @@ export type View =
| 'configureProviders'
| 'configPage';

export type ViewConfig = {
view: View;
viewOptions?: SettingsViewOptions | Record<any, any>;

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

View workflow job for this annotation

GitHub Actions / Lint Electron Desktop App

Unexpected any. Specify a different type

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

View workflow job for this annotation

GitHub Actions / Lint Electron Desktop App

Unexpected any. Specify a different type
};

export default function App() {
const [fatalError, setFatalError] = useState<string | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const [pendingLink, setPendingLink] = useState<string | null>(null);
const [modalMessage, setModalMessage] = useState<string>('');
const [isInstalling, setIsInstalling] = useState(false);
const [view, setView] = useState<View>('welcome');
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>({
view: 'welcome',
viewOptions: {},
});

const { switchModel } = useModel();
const { addRecentModel } = useRecentModels();
const setView = (view: View, viewOptions: Record<any, any> = {}) => {

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

View workflow job for this annotation

GitHub Actions / Lint Electron Desktop App

Unexpected any. Specify a different type

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

View workflow job for this annotation

GitHub Actions / Lint Electron Desktop App

Unexpected any. Specify a different type
setInternalView({ view, viewOptions });
};

// Utility function to extract the command from the link
function extractCommand(link: string): string {
Expand Down Expand Up @@ -193,6 +206,7 @@ export default function App() {
setView('chat');
}}
setView={setView}
viewOptions={viewOptions as SettingsViewOptions}
/>
)}
{view === 'moreModels' && (
Expand Down
15 changes: 10 additions & 5 deletions ui/desktop/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,20 @@ const DEFAULT_SETTINGS: SettingsType = {
extensions: BUILT_IN_EXTENSIONS,
};

export type SettingsViewOptions = {
extensionId: string;
showEnvVars: boolean;
};

export default function SettingsView({
onClose,
setView,
viewOptions,
}: {
onClose: () => void;
setView: (view: View) => void;
viewOptions: SettingsViewOptions;
}) {
const [searchParams] = useState(() => new URLSearchParams(window.location.search));

const [settings, setSettings] = React.useState<SettingsType>(() => {
const saved = localStorage.getItem('user_settings');
window.electron.logInfo('Settings: ' + saved);
Expand Down Expand Up @@ -101,10 +106,10 @@ export default function SettingsView({

// Handle URL parameters for auto-opening extension configuration
useEffect(() => {
const extensionId = searchParams.get('extensionId');
const showEnvVars = searchParams.get('showEnvVars');
const extensionId = viewOptions.extensionId;
const showEnvVars = viewOptions.showEnvVars;

if (extensionId && showEnvVars === 'true') {
if (extensionId && showEnvVars === true) {
// Find the extension in settings
const extension = settings.extensions.find((ext) => ext.id === extensionId);
if (extension) {
Expand Down
12 changes: 7 additions & 5 deletions ui/desktop/src/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getApiUrl, getSecretKey } from './config';
import { type View } from './App';
import { type SettingsViewOptions } from './components/settings/SettingsView';
import { toast } from 'react-toastify';

// ExtensionConfig type matching the Rust version
Expand Down Expand Up @@ -194,7 +195,7 @@ function storeExtensionConfig(config: FullExtensionConfig) {
localStorage.setItem('user_settings', JSON.stringify(userSettings));
console.log('Extension config stored successfully in user_settings');
// Notify settings update through electron IPC
window.electron.send('settings-updated');
window.electron.emit('settings-updated');
} else {
console.log('Extension config already exists in user_settings');
}
Expand Down Expand Up @@ -258,7 +259,10 @@ function handleError(message: string, shouldThrow = false): void {
}
}

export async function addExtensionFromDeepLink(url: string, setView: (view: View) => void) {
export async function addExtensionFromDeepLink(
url: string,
setView: (view: View, options: SettingsViewOptions) => void
) {
if (!url.startsWith('goose://extension')) {
handleError(
'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
Expand Down Expand Up @@ -344,9 +348,7 @@ export async function addExtensionFromDeepLink(url: string, setView: (view: View
// Check if extension requires env vars and go to settings if so
if (envVarsRequired(config)) {
console.log('Environment variables required, redirecting to settings');
setView('settings');
// TODO - add code which can auto-open the modal on the settings view
// navigate(`/settings?extensionId=${config.id}&showEnvVars=true`);
setView('settings', { extensionId: config.id, showEnvVars: true });
return;
}

Expand Down
72 changes: 1 addition & 71 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from 'electron';
import started from 'electron-squirrel-startup';
import path from 'node:path';
import { handleSquirrelEvent } from './setup-events';
import { startGoosed } from './goosed';
import { getBinaryPath } from './utils/binaryPath';
import { loadShellEnv } from './utils/loadEnv';
Expand All @@ -34,82 +33,13 @@ import { promisify } from 'util';

const exec = promisify(execCallback);

// Handle Squirrel events for Windows installer
if (process.platform === 'win32') {
console.log('Windows detected, command line args:', process.argv);

if (handleSquirrelEvent()) {
// squirrel event handled and app will exit in 1000ms, so don't do anything else
process.exit(0);
}

// Handle the protocol on Windows during first launch
if (process.argv.length >= 2) {
const url = process.argv[1];
console.log('Checking URL from command line:', url);
if (url.startsWith('goose://')) {
console.log('Found goose:// URL in command line args');
app.emit('open-url', { preventDefault: () => {} }, url);
}
}
}

// Ensure single instance lock
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, _workingDirectory) => {
// Someone tried to run a second instance
console.log('Second instance detected with args:', commandLine);

// Get existing window or create new one
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
const window = existingWindows[0];
if (window.isMinimized()) window.restore();
window.focus();

if (process.platform === 'win32') {
// Protocol handling for Windows
const url = commandLine[commandLine.length - 1];
console.log('Checking last arg for protocol:', url);
if (url.startsWith('goose://')) {
console.log('Found goose:// URL in second instance');
// Send the URL to the window
if (!window.webContents.isLoading()) {
window.webContents.send('add-extension', url);
} else {
window.webContents.once('did-finish-load', () => {
window.webContents.send('add-extension', url);
});
}
}
}
}
});
}

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) app.quit();

// Register protocol handler
if (process.platform === 'win32') {
const success = app.setAsDefaultProtocolClient('goose', process.execPath, ['--']);
console.log('Registering protocol handler for Windows:', success ? 'success' : 'failed');
} else {
const success = app.setAsDefaultProtocolClient('goose');
console.log('Registering protocol handler:', success ? 'success' : 'failed');
}

// Log if we're the default protocol handler
console.log('Is default protocol handler:', app.isDefaultProtocolClient('goose'));
app.setAsDefaultProtocolClient('goose');

// Triggered when the user opens "goose://..." links
app.on('open-url', async (event, url) => {
event.preventDefault();
console.log('open-url:', url);

// Get existing window or create new one
let targetWindow: BrowserWindow;
Expand Down
4 changes: 4 additions & 0 deletions ui/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ElectronAPI = {
channel: string,
callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
) => void;
emit: (channel: string, ...args: any[]) => void;
};

type AppConfigAPI = {
Expand Down Expand Up @@ -55,6 +56,9 @@ const electronAPI: ElectronAPI = {
off: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
ipcRenderer.off(channel, callback);
},
emit: (channel: string, ...args: any[]) => {
ipcRenderer.emit(channel, ...args);
},
};

const appConfigAPI: AppConfigAPI = {
Expand Down

0 comments on commit ffe020e

Please sign in to comment.