Add support for changing working dir and extensions in same window/session (#6057)

This commit is contained in:
Zane
2026-01-08 16:48:15 -08:00
committed by GitHub
parent 4a60f026d5
commit 9a01fcb740
49 changed files with 1865 additions and 1204 deletions
@@ -2,6 +2,9 @@
* Shared constants and utilities for extension error handling
*/
import { ExtensionLoadResult } from '../api/types.gen';
import { toastService, ExtensionLoadingStatus } from '../toasts';
export const MAX_ERROR_MESSAGE_LENGTH = 70;
/**
@@ -28,3 +31,43 @@ export function formatExtensionErrorMessage(
): string {
return errorMsg.length < MAX_ERROR_MESSAGE_LENGTH ? errorMsg : fallback;
}
/**
* Shows toast notifications for extension load results.
* Uses grouped toast for multiple extensions, individual error toast for single failed extension.
* @param results - Array of extension load results from the backend
*/
export function showExtensionLoadResults(results: ExtensionLoadResult[] | null | undefined): void {
if (!results || results.length === 0) {
return;
}
const failedExtensions = results.filter((r) => !r.success);
if (results.length === 1 && failedExtensions.length === 1) {
const failed = failedExtensions[0];
const errorMsg = failed.error || 'Unknown error';
const recoverHints = createExtensionRecoverHints(errorMsg);
const displayMsg = formatExtensionErrorMessage(errorMsg, 'Failed to load extension');
toastService.error({
title: failed.name,
msg: displayMsg,
traceback: errorMsg,
recoverHints,
});
return;
}
const extensionStatuses: ExtensionLoadingStatus[] = results.map((r) => {
const errorMsg = r.error || 'Unknown error';
return {
name: r.name,
status: r.success ? 'success' : 'error',
error: r.success ? undefined : errorMsg,
recoverHints: r.success ? undefined : createExtensionRecoverHints(errorMsg),
};
});
toastService.extensionLoading(extensionStatuses, results.length, true);
}
-3
View File
@@ -19,9 +19,7 @@ export type View =
| 'recipes'
| 'permission';
// TODO(Douwe): check these for usage, especially key: string for resetChat
export type ViewOptions = {
extensionId?: string;
showEnvVars?: boolean;
deepLinkConfig?: unknown;
sessionDetails?: unknown;
@@ -32,7 +30,6 @@ export type ViewOptions = {
parentViewOptions?: ViewOptions;
disableAnimation?: boolean;
initialMessage?: string;
resetChat?: boolean;
shareToken?: string;
resumeSessionId?: string;
pendingScheduleDeepLink?: string;
-89
View File
@@ -1,13 +1,9 @@
import {
initializeBundledExtensions,
syncBundledExtensions,
addToAgentOnStartup,
} from '../components/settings/extensions';
import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigContext';
import { Recipe, updateAgentProvider, updateFromSession } from '../api';
import { toastService, ExtensionLoadingStatus } from '../toasts';
import { errorMessage } from './conversionUtils';
import { createExtensionRecoverHints } from './extensionErrorUtils';
// Helper function to substitute parameters in text
export const substituteParameters = (text: string, params: Record<string, string>): string => {
@@ -29,7 +25,6 @@ export const initializeSystem = async (
options?: {
getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>;
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
setIsExtensionsLoading?: (loading: boolean) => void;
recipeParameters?: Record<string, string> | null;
recipe?: Recipe;
}
@@ -72,95 +67,11 @@ export const initializeSystem = async (
if (refreshedExtensions.length === 0) {
await initializeBundledExtensions(options.addExtension);
refreshedExtensions = await options.getExtensions(false);
} else {
await syncBundledExtensions(refreshedExtensions, options.addExtension);
}
// Add enabled extensions to agent in parallel
const enabledExtensions = refreshedExtensions.filter((ext) => ext.enabled);
if (enabledExtensions.length === 0) {
return;
}
options?.setIsExtensionsLoading?.(true);
// Initialize extension status tracking
const extensionStatuses: Map<string, ExtensionLoadingStatus> = new Map(
enabledExtensions.map((ext) => [ext.name, { name: ext.name, status: 'loading' as const }])
);
// Show initial loading toast
const updateToast = (isComplete: boolean = false) => {
toastService.extensionLoading(
Array.from(extensionStatuses.values()),
enabledExtensions.length,
isComplete
);
};
updateToast();
// Load extensions in parallel and update status
const extensionLoadingPromises = enabledExtensions.map(async (extensionConfig) => {
const extensionName = extensionConfig.name;
// SSE is unsupported - fail immediately without calling the backend
if (extensionConfig.type === 'sse') {
const errMsg = 'SSE is unsupported, migrate to streamable_http';
extensionStatuses.set(extensionName, {
name: extensionName,
status: 'error',
error: errMsg,
recoverHints: createExtensionRecoverHints(errMsg),
});
updateToast();
return;
}
try {
await addToAgentOnStartup({
extensionConfig,
toastOptions: { silent: true }, // Silent since we're using grouped notification
sessionId,
});
// Update status to success
extensionStatuses.set(extensionName, {
name: extensionName,
status: 'success',
});
updateToast();
} catch (error) {
console.error(`Failed to load extension ${extensionName}:`, error);
// Extract error message using shared utility
const errMsg = errorMessage(error);
// Create recovery hints for "Ask goose" button
const recoverHints = createExtensionRecoverHints(errMsg);
// Update status to error
extensionStatuses.set(extensionName, {
name: extensionName,
status: 'error',
error: errMsg,
recoverHints,
});
updateToast();
}
});
await Promise.allSettled(extensionLoadingPromises);
// Show final completion toast
updateToast(true);
options?.setIsExtensionsLoading?.(false);
} catch (error) {
console.error('Failed to initialize agent:', error);
options?.setIsExtensionsLoading?.(false);
throw error;
}
};
+3
View File
@@ -0,0 +1,3 @@
export const getInitialWorkingDir = (): string => {
return (window.appConfig?.get('GOOSE_WORKING_DIR') as string) || '';
};