fix: strip empty extensions array when deeplink also (#7096)
This commit is contained in:
+3
-17
@@ -83,23 +83,17 @@ const PairRouteWrapper = ({
|
||||
const [isCreatingSession, setIsCreatingSession] = useState(false);
|
||||
|
||||
const resumeSessionId = searchParams.get('resumeSessionId') ?? undefined;
|
||||
const recipeId = searchParams.get('recipeId') ?? undefined;
|
||||
const recipeDeeplinkFromConfig = window.appConfig?.get('recipeDeeplink') as string | undefined;
|
||||
const initialMessage = routeState.initialMessage;
|
||||
|
||||
// Create session if we have an initialMessage, recipeId, or recipeDeeplink but no sessionId
|
||||
// Create session if we have an initialMessage or recipeDeeplink but no sessionId
|
||||
useEffect(() => {
|
||||
if (
|
||||
(initialMessage || recipeId || recipeDeeplinkFromConfig) &&
|
||||
!resumeSessionId &&
|
||||
!isCreatingSession
|
||||
) {
|
||||
if ((initialMessage || recipeDeeplinkFromConfig) && !resumeSessionId && !isCreatingSession) {
|
||||
setIsCreatingSession(true);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const newSession = await createSession(getInitialWorkingDir(), {
|
||||
recipeId,
|
||||
recipeDeeplink: recipeDeeplinkFromConfig,
|
||||
allExtensions: extensionsList,
|
||||
});
|
||||
@@ -115,7 +109,6 @@ const PairRouteWrapper = ({
|
||||
|
||||
setSearchParams((prev) => {
|
||||
prev.set('resumeSessionId', newSession.id);
|
||||
prev.delete('recipeId');
|
||||
return prev;
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -133,14 +126,7 @@ const PairRouteWrapper = ({
|
||||
// Note: isCreatingSession is intentionally NOT in the dependency array
|
||||
// It's only used as a guard to prevent concurrent session creation
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
initialMessage,
|
||||
recipeId,
|
||||
recipeDeeplinkFromConfig,
|
||||
resumeSessionId,
|
||||
setSearchParams,
|
||||
extensionsList,
|
||||
]);
|
||||
}, [initialMessage, recipeDeeplinkFromConfig, resumeSessionId, setSearchParams, extensionsList]);
|
||||
|
||||
// Add resumed session to active sessions if not already there
|
||||
useEffect(() => {
|
||||
|
||||
@@ -40,11 +40,17 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleExtensionsLoaded = () => {
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
};
|
||||
|
||||
window.addEventListener(AppEvents.SESSION_CREATED, handleSessionLoaded);
|
||||
window.addEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, handleExtensionsLoaded);
|
||||
window.addEventListener(AppEvents.MESSAGE_STREAM_FINISHED, handleSessionLoaded);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(AppEvents.SESSION_CREATED, handleSessionLoaded);
|
||||
window.removeEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, handleExtensionsLoaded);
|
||||
window.removeEventListener(AppEvents.MESSAGE_STREAM_FINISHED, handleSessionLoaded);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from '../../api';
|
||||
import ImportRecipeForm, { ImportRecipeButton } from './ImportRecipeForm';
|
||||
import CreateEditRecipeModal from './CreateEditRecipeModal';
|
||||
import { generateDeepLink, Recipe, stripEmptyExtensions } from '../../recipe';
|
||||
import { generateDeepLink, encodeRecipe, Recipe, stripEmptyExtensions } from '../../recipe';
|
||||
import { useNavigation } from '../../hooks/useNavigation';
|
||||
import { CronPicker } from '../schedule/CronPicker';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
|
||||
@@ -165,15 +165,16 @@ export default function RecipesView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartRecipeChatInNewWindow = (recipeId: string) => {
|
||||
const handleStartRecipeChatInNewWindow = async (recipe: Recipe) => {
|
||||
try {
|
||||
const encodedRecipe = await encodeRecipe(stripEmptyExtensions(recipe) as Recipe);
|
||||
window.electron.createChatWindow(
|
||||
undefined,
|
||||
getInitialWorkingDir(),
|
||||
undefined,
|
||||
undefined,
|
||||
'pair',
|
||||
recipeId
|
||||
encodedRecipe
|
||||
);
|
||||
trackRecipeStarted(true, undefined, true);
|
||||
} catch (error) {
|
||||
@@ -521,7 +522,7 @@ export default function RecipesView() {
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleStartRecipeChatInNewWindow(recipeManifestResponse.id);
|
||||
handleStartRecipeChatInNewWindow(recipe);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
export enum AppEvents {
|
||||
SESSION_CREATED = 'session-created',
|
||||
SESSION_EXTENSIONS_LOADED = 'session-extensions-loaded',
|
||||
SESSION_DELETED = 'session-deleted',
|
||||
SESSION_RENAMED = 'session-renamed',
|
||||
SESSION_FORKED = 'session-forked',
|
||||
|
||||
+5
-22
@@ -157,7 +157,6 @@ if (process.platform !== 'darwin') {
|
||||
undefined,
|
||||
deeplinkData?.config,
|
||||
scheduledJobId || undefined,
|
||||
undefined,
|
||||
deeplinkData?.parameters
|
||||
);
|
||||
});
|
||||
@@ -258,7 +257,6 @@ async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) {
|
||||
undefined,
|
||||
deeplinkData?.config,
|
||||
scheduledJobId || undefined,
|
||||
undefined,
|
||||
deeplinkData?.parameters
|
||||
);
|
||||
pendingDeepLink = null;
|
||||
@@ -297,7 +295,6 @@ app.on('open-url', async (_event, url) => {
|
||||
undefined,
|
||||
deeplinkData?.config,
|
||||
scheduledJobId || undefined,
|
||||
undefined,
|
||||
deeplinkData?.parameters
|
||||
);
|
||||
windowDeeplinkURL = null;
|
||||
@@ -480,7 +477,6 @@ const createChat = async (
|
||||
viewType?: string,
|
||||
recipeDeeplink?: string, // Raw deeplink decoded on server
|
||||
scheduledJobId?: string, // Scheduled job ID if applicable
|
||||
recipeId?: string,
|
||||
recipeParameters?: Record<string, string> // Recipe parameter values from deeplink URL
|
||||
) => {
|
||||
const settings = getSettings();
|
||||
@@ -528,7 +524,6 @@ const createChat = async (
|
||||
REQUEST_DIR: dir,
|
||||
GOOSE_BASE_URL_SHARE: baseUrlShare,
|
||||
GOOSE_VERSION: version,
|
||||
recipeId: recipeId,
|
||||
recipeDeeplink: recipeDeeplink,
|
||||
recipeParameters: recipeParameters,
|
||||
scheduledJobId: scheduledJobId,
|
||||
@@ -711,10 +706,7 @@ const createChat = async (
|
||||
if (viewType) {
|
||||
appPath = routeMap[viewType] || '/';
|
||||
}
|
||||
if (
|
||||
appPath === '/' &&
|
||||
(recipeDeeplink !== undefined || recipeId !== undefined || initialMessage)
|
||||
) {
|
||||
if (appPath === '/' && (recipeDeeplink !== undefined || initialMessage)) {
|
||||
appPath = '/pair';
|
||||
}
|
||||
|
||||
@@ -725,14 +717,6 @@ const createChat = async (
|
||||
appPath = '/pair';
|
||||
}
|
||||
}
|
||||
// Only add recipeId to URL for the non-deeplink case (saved recipes launched from UI)
|
||||
// For deeplinks, the recipe object is passed via appConfig, not URL params
|
||||
if (recipeId) {
|
||||
searchParams.set('recipeId', recipeId);
|
||||
if (appPath === '/') {
|
||||
appPath = '/pair';
|
||||
}
|
||||
}
|
||||
|
||||
// Goose's react app uses HashRouter, so the path + search params follow a #/
|
||||
url.hash = `${appPath}?${searchParams.toString()}`;
|
||||
@@ -1064,7 +1048,6 @@ const openDirectoryDialog = async (): Promise<OpenDialogReturnValue> => {
|
||||
undefined,
|
||||
deeplinkData?.config,
|
||||
undefined,
|
||||
undefined,
|
||||
deeplinkData?.parameters
|
||||
);
|
||||
}
|
||||
@@ -2018,13 +2001,13 @@ async function appMain() {
|
||||
|
||||
ipcMain.on(
|
||||
'create-chat-window',
|
||||
(event, query, dir, version, resumeSessionId, viewType, recipeId) => {
|
||||
(event, query, dir, version, resumeSessionId, viewType, recipeDeeplink) => {
|
||||
if (!dir?.trim()) {
|
||||
const recentDirs = loadRecentDirs();
|
||||
dir = recentDirs.length > 0 ? recentDirs[0] : undefined;
|
||||
}
|
||||
|
||||
const isFromLauncher = query && !resumeSessionId && !viewType && !recipeId;
|
||||
const isFromLauncher = query && !resumeSessionId && !viewType && !recipeDeeplink;
|
||||
|
||||
if (isFromLauncher) {
|
||||
const senderWindow = BrowserWindow.fromWebContents(event.sender);
|
||||
@@ -2052,9 +2035,9 @@ async function appMain() {
|
||||
version,
|
||||
resumeSessionId,
|
||||
viewType,
|
||||
recipeDeeplink,
|
||||
undefined,
|
||||
undefined,
|
||||
recipeId
|
||||
undefined
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ type ElectronAPI = {
|
||||
version?: string,
|
||||
resumeSessionId?: string,
|
||||
viewType?: string,
|
||||
recipeId?: string
|
||||
recipeDeeplink?: string
|
||||
) => void;
|
||||
logInfo: (txt: string) => void;
|
||||
showNotification: (data: NotificationData) => void;
|
||||
@@ -156,7 +156,7 @@ const electronAPI: ElectronAPI = {
|
||||
version?: string,
|
||||
resumeSessionId?: string,
|
||||
viewType?: string,
|
||||
recipeId?: string
|
||||
recipeDeeplink?: string
|
||||
) =>
|
||||
ipcRenderer.send(
|
||||
'create-chat-window',
|
||||
@@ -165,7 +165,7 @@ const electronAPI: ElectronAPI = {
|
||||
version,
|
||||
resumeSessionId,
|
||||
viewType,
|
||||
recipeId
|
||||
recipeDeeplink
|
||||
),
|
||||
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
|
||||
showNotification: (data: NotificationData) => ipcRenderer.send('notify', data),
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function decodeRecipe(deeplink: string): Promise<Recipe> {
|
||||
throw new Error('Decoded recipe is null');
|
||||
}
|
||||
|
||||
return response.data.recipe as Recipe;
|
||||
return stripEmptyExtensions(response.data.recipe as Recipe);
|
||||
} catch (error) {
|
||||
console.error('Failed to decode deeplink:', error);
|
||||
throw error;
|
||||
@@ -78,6 +78,19 @@ export async function generateDeepLink(recipe: Recipe): Promise<string> {
|
||||
return `goose://recipe?config=${encoded}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips empty extensions arrays from recipes before passing to the backend.
|
||||
*
|
||||
* This is a backwards compatibility workaround for the desktop app. Previously,
|
||||
* the UI was saving recipes with an empty `extensions: []` array, which the
|
||||
* backend interprets as "use no extensions" rather than "use user's default
|
||||
* extensions". By removing the empty array, the backend will fall back to
|
||||
* loading the user's configured default extensions.
|
||||
*
|
||||
* This can be removed once we have the ability to manage recipe extensions
|
||||
* directly in the UI, allowing users to explicitly choose which extensions
|
||||
* a recipe should use.
|
||||
*/
|
||||
export function stripEmptyExtensions(recipe: Recipe): Recipe {
|
||||
if (Array.isArray(recipe.extensions) && recipe.extensions.length === 0) {
|
||||
const { extensions: _, ...rest } = recipe;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from './store/extensionOverrides';
|
||||
import type { FixedExtensionEntry } from './components/ConfigContext';
|
||||
import { AppEvents } from './constants/events';
|
||||
import { decodeRecipe, Recipe } from './recipe';
|
||||
|
||||
export function shouldShowNewChatTitle(session: Session): boolean {
|
||||
if (session.recipe) {
|
||||
@@ -36,7 +37,6 @@ export function resumeSession(session: Session, setView: setViewType) {
|
||||
export async function createSession(
|
||||
workingDir: string,
|
||||
options?: {
|
||||
recipeId?: string;
|
||||
recipeDeeplink?: string;
|
||||
extensionConfigs?: ExtensionConfig[];
|
||||
allExtensions?: FixedExtensionEntry[];
|
||||
@@ -44,17 +44,14 @@ export async function createSession(
|
||||
): Promise<Session> {
|
||||
const body: {
|
||||
working_dir: string;
|
||||
recipe_id?: string;
|
||||
recipe_deeplink?: string;
|
||||
recipe?: Recipe;
|
||||
extension_overrides?: ExtensionConfig[];
|
||||
} = {
|
||||
working_dir: workingDir,
|
||||
};
|
||||
|
||||
if (options?.recipeId) {
|
||||
body.recipe_id = options.recipeId;
|
||||
} else if (options?.recipeDeeplink) {
|
||||
body.recipe_deeplink = options.recipeDeeplink;
|
||||
if (options?.recipeDeeplink) {
|
||||
body.recipe = await decodeRecipe(options.recipeDeeplink);
|
||||
}
|
||||
|
||||
if (options?.extensionConfigs && options.extensionConfigs.length > 0) {
|
||||
@@ -81,15 +78,15 @@ export async function startNewSession(
|
||||
setView: setViewType,
|
||||
workingDir: string,
|
||||
options?: {
|
||||
recipeId?: string;
|
||||
recipeDeeplink?: string;
|
||||
allExtensions?: FixedExtensionEntry[];
|
||||
}
|
||||
): Promise<Session> {
|
||||
const session = await createSession(workingDir, options);
|
||||
|
||||
// Include session data so sidebar can add it immediately (before it has messages)
|
||||
window.dispatchEvent(new CustomEvent(AppEvents.SESSION_CREATED, { detail: { session } }));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED, { detail: { session } })
|
||||
);
|
||||
|
||||
const initialMessage = initialText ? { msg: initialText, images: [] } : undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user