fix: deep link installs of extensions (#1333)
This commit is contained in:
+16
-2
@@ -13,12 +13,13 @@ import { extractExtensionName } from './components/settings/extensions/utils';
|
|||||||
|
|
||||||
import WelcomeView from './components/WelcomeView';
|
import WelcomeView from './components/WelcomeView';
|
||||||
import ChatView from './components/ChatView';
|
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 MoreModelsView from './components/settings/models/MoreModelsView';
|
||||||
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
|
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
// Views and their options
|
||||||
export type View =
|
export type View =
|
||||||
| 'welcome'
|
| 'welcome'
|
||||||
| 'chat'
|
| 'chat'
|
||||||
@@ -27,15 +28,27 @@ export type View =
|
|||||||
| 'configureProviders'
|
| 'configureProviders'
|
||||||
| 'configPage';
|
| 'configPage';
|
||||||
|
|
||||||
|
export type ViewConfig = {
|
||||||
|
view: View;
|
||||||
|
viewOptions?: SettingsViewOptions | Record<any, any>;
|
||||||
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [fatalError, setFatalError] = useState<string | null>(null);
|
const [fatalError, setFatalError] = useState<string | null>(null);
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
||||||
const [modalMessage, setModalMessage] = useState<string>('');
|
const [modalMessage, setModalMessage] = useState<string>('');
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
const [view, setView] = useState<View>('welcome');
|
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>({
|
||||||
|
view: 'welcome',
|
||||||
|
viewOptions: {},
|
||||||
|
});
|
||||||
|
|
||||||
const { switchModel } = useModel();
|
const { switchModel } = useModel();
|
||||||
const { addRecentModel } = useRecentModels();
|
const { addRecentModel } = useRecentModels();
|
||||||
|
const setView = (view: View, viewOptions: Record<any, any> = {}) => {
|
||||||
|
setInternalView({ view, viewOptions });
|
||||||
|
};
|
||||||
|
|
||||||
// Utility function to extract the command from the link
|
// Utility function to extract the command from the link
|
||||||
function extractCommand(link: string): string {
|
function extractCommand(link: string): string {
|
||||||
@@ -193,6 +206,7 @@ export default function App() {
|
|||||||
setView('chat');
|
setView('chat');
|
||||||
}}
|
}}
|
||||||
setView={setView}
|
setView={setView}
|
||||||
|
viewOptions={viewOptions as SettingsViewOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view === 'moreModels' && (
|
{view === 'moreModels' && (
|
||||||
|
|||||||
@@ -46,15 +46,20 @@ const DEFAULT_SETTINGS: SettingsType = {
|
|||||||
extensions: BUILT_IN_EXTENSIONS,
|
extensions: BUILT_IN_EXTENSIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SettingsViewOptions = {
|
||||||
|
extensionId: string;
|
||||||
|
showEnvVars: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default function SettingsView({
|
export default function SettingsView({
|
||||||
onClose,
|
onClose,
|
||||||
setView,
|
setView,
|
||||||
|
viewOptions,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
setView: (view: View) => void;
|
setView: (view: View) => void;
|
||||||
|
viewOptions: SettingsViewOptions;
|
||||||
}) {
|
}) {
|
||||||
const [searchParams] = useState(() => new URLSearchParams(window.location.search));
|
|
||||||
|
|
||||||
const [settings, setSettings] = React.useState<SettingsType>(() => {
|
const [settings, setSettings] = React.useState<SettingsType>(() => {
|
||||||
const saved = localStorage.getItem('user_settings');
|
const saved = localStorage.getItem('user_settings');
|
||||||
window.electron.logInfo('Settings: ' + saved);
|
window.electron.logInfo('Settings: ' + saved);
|
||||||
@@ -101,10 +106,10 @@ export default function SettingsView({
|
|||||||
|
|
||||||
// Handle URL parameters for auto-opening extension configuration
|
// Handle URL parameters for auto-opening extension configuration
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const extensionId = searchParams.get('extensionId');
|
const extensionId = viewOptions.extensionId;
|
||||||
const showEnvVars = searchParams.get('showEnvVars');
|
const showEnvVars = viewOptions.showEnvVars;
|
||||||
|
|
||||||
if (extensionId && showEnvVars === 'true') {
|
if (extensionId && showEnvVars === true) {
|
||||||
// Find the extension in settings
|
// Find the extension in settings
|
||||||
const extension = settings.extensions.find((ext) => ext.id === extensionId);
|
const extension = settings.extensions.find((ext) => ext.id === extensionId);
|
||||||
if (extension) {
|
if (extension) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getApiUrl, getSecretKey } from './config';
|
import { getApiUrl, getSecretKey } from './config';
|
||||||
import { type View } from './App';
|
import { type View } from './App';
|
||||||
|
import { type SettingsViewOptions } from './components/settings/SettingsView';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
// ExtensionConfig type matching the Rust version
|
// ExtensionConfig type matching the Rust version
|
||||||
@@ -194,7 +195,7 @@ function storeExtensionConfig(config: FullExtensionConfig) {
|
|||||||
localStorage.setItem('user_settings', JSON.stringify(userSettings));
|
localStorage.setItem('user_settings', JSON.stringify(userSettings));
|
||||||
console.log('Extension config stored successfully in user_settings');
|
console.log('Extension config stored successfully in user_settings');
|
||||||
// Notify settings update through electron IPC
|
// Notify settings update through electron IPC
|
||||||
window.electron.send('settings-updated');
|
window.electron.emit('settings-updated');
|
||||||
} else {
|
} else {
|
||||||
console.log('Extension config already exists in user_settings');
|
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')) {
|
if (!url.startsWith('goose://extension')) {
|
||||||
handleError(
|
handleError(
|
||||||
'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
|
'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
|
// Check if extension requires env vars and go to settings if so
|
||||||
if (envVarsRequired(config)) {
|
if (envVarsRequired(config)) {
|
||||||
console.log('Environment variables required, redirecting to settings');
|
console.log('Environment variables required, redirecting to settings');
|
||||||
setView('settings');
|
setView('settings', { extensionId: config.id, showEnvVars: true });
|
||||||
// TODO - add code which can auto-open the modal on the settings view
|
|
||||||
// navigate(`/settings?extensionId=${config.id}&showEnvVars=true`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-71
@@ -14,7 +14,6 @@ import {
|
|||||||
} from 'electron';
|
} from 'electron';
|
||||||
import started from 'electron-squirrel-startup';
|
import started from 'electron-squirrel-startup';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { handleSquirrelEvent } from './setup-events';
|
|
||||||
import { startGoosed } from './goosed';
|
import { startGoosed } from './goosed';
|
||||||
import { getBinaryPath } from './utils/binaryPath';
|
import { getBinaryPath } from './utils/binaryPath';
|
||||||
import { loadShellEnv } from './utils/loadEnv';
|
import { loadShellEnv } from './utils/loadEnv';
|
||||||
@@ -34,82 +33,13 @@ import { promisify } from 'util';
|
|||||||
|
|
||||||
const exec = promisify(execCallback);
|
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();
|
if (started) app.quit();
|
||||||
|
|
||||||
// Register protocol handler
|
app.setAsDefaultProtocolClient('goose');
|
||||||
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'));
|
|
||||||
|
|
||||||
// Triggered when the user opens "goose://..." links
|
// Triggered when the user opens "goose://..." links
|
||||||
app.on('open-url', async (event, url) => {
|
app.on('open-url', async (event, url) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log('open-url:', url);
|
|
||||||
|
|
||||||
// Get existing window or create new one
|
// Get existing window or create new one
|
||||||
let targetWindow: BrowserWindow;
|
let targetWindow: BrowserWindow;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type ElectronAPI = {
|
|||||||
channel: string,
|
channel: string,
|
||||||
callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
|
||||||
) => void;
|
) => void;
|
||||||
|
emit: (channel: string, ...args: any[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppConfigAPI = {
|
type AppConfigAPI = {
|
||||||
@@ -55,6 +56,9 @@ const electronAPI: ElectronAPI = {
|
|||||||
off: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
off: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
||||||
ipcRenderer.off(channel, callback);
|
ipcRenderer.off(channel, callback);
|
||||||
},
|
},
|
||||||
|
emit: (channel: string, ...args: any[]) => {
|
||||||
|
ipcRenderer.emit(channel, ...args);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const appConfigAPI: AppConfigAPI = {
|
const appConfigAPI: AppConfigAPI = {
|
||||||
|
|||||||
Reference in New Issue
Block a user