fix: deep link installs of extensions (#1333)

This commit is contained in:
Alex Hancock
2025-02-21 15:15:05 -05:00
committed by GitHub
parent 78f450085a
commit ffe020ea0c
5 changed files with 38 additions and 83 deletions

View File

@@ -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'
@@ -27,15 +28,27 @@ export type View =
| 'configureProviders'
| 'configPage';
export type ViewConfig = {
view: View;
viewOptions?: SettingsViewOptions | Record<any, any>;
};
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> = {}) => {
setInternalView({ view, viewOptions });
};
// Utility function to extract the command from the link
function extractCommand(link: string): string {
@@ -193,6 +206,7 @@ export default function App() {
setView('chat');
}}
setView={setView}
viewOptions={viewOptions as SettingsViewOptions}
/>
)}
{view === 'moreModels' && (

View File

@@ -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);
@@ -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) {

View File

@@ -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
@@ -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');
}
@@ -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'
@@ -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;
}

View File

@@ -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';
@@ -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;

View File

@@ -26,6 +26,7 @@ type ElectronAPI = {
channel: string,
callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
) => void;
emit: (channel: string, ...args: any[]) => void;
};
type AppConfigAPI = {
@@ -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 = {