remove false optionality in desktop props (#12776)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Douwe Osinga 2026-10-13 14:02:10 +00:00 • committed by GitHub
parent 6ead9cb601
commit 91ecd8de7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 336 additions and 568 deletions

View file

@ -224,8 +224,7 @@ const SessionsRoute = () => {
};
const SchedulesRoute = () => {
const navigate = useNavigate();
return <SchedulesView onClose={() => navigate('/')} />;
return <SchedulesView />;
};
const RecipesRoute = () => {

View file

@ -88,7 +88,7 @@ function updateRequestToCreate(
models: request.models,
supportsStreaming: request.supports_streaming ?? null,
headers: request.headers ?? undefined,
requiresAuth: request.requires_auth ?? true,
requiresAuth: request.requires_auth,
catalogProviderId: request.catalog_provider_id ?? null,
basePath: request.base_path ?? null,
toolshim: request.toolshim,
@ -324,9 +324,9 @@ export async function acpReadDefaults(): Promise<{
};
}
export async function acpSaveDefaults(providerId: string, modelId?: string | null): Promise<void> {
export async function acpSaveDefaults(providerId: string, modelId: string | null): Promise<void> {
const client = await getAcpClient();
await client.goose.defaultsSave_unstable({ providerId, modelId: modelId ?? null });
await client.goose.defaultsSave_unstable({ providerId, modelId });
}
export async function acpClearDefaults(): Promise<void> {

View file

@ -126,7 +126,7 @@ export async function deleteRecipe(id: string): Promise<void> {
}
}
export async function scheduleRecipe(id: string, cronSchedule?: string | null): Promise<void> {
export async function scheduleRecipe(id: string, cronSchedule: string | null): Promise<void> {
try {
const client = await getAcpClient();
await client.goose.recipesSchedule_unstable({ id, cron_schedule: cronSchedule });
@ -137,7 +137,7 @@ export async function scheduleRecipe(id: string, cronSchedule?: string | null):
export async function setRecipeSlashCommand(
id: string,
slashCommand?: string | null
slashCommand: string | null
): Promise<void> {
try {
const client = await getAcpClient();

View file

@ -62,12 +62,6 @@ const isUserMessage = (message: Message) => message.role === 'user';
interface BaseChatProps {
setChat: (chat: ChatType) => void;
onMessageSubmit?: (message: string) => void;
renderHeader?: () => React.ReactNode;
customChatInputProps?: Record<string, unknown>;
customMainLayoutProps?: Record<string, unknown>;
contentClassName?: string;
disableSearch?: boolean;
suppressEmptyState: boolean;
sessionId: string;
isActiveSession: boolean;
@ -78,9 +72,6 @@ interface BaseChatProps {
export default function BaseChat({
setChat,
renderHeader,
customChatInputProps = {},
customMainLayoutProps = {},
sessionId,
initialMessage,
noAutoSubmit,
@ -135,10 +126,7 @@ export default function BaseChat({
setQuoteButtonPos(null);
return;
}
if (
!conversationRef.current ||
!conversationRef.current.contains(sel.anchorNode)
) {
if (!conversationRef.current || !conversationRef.current.contains(sel.anchorNode)) {
setQuoteButtonPos(null);
return;
}
@ -439,12 +427,7 @@ export default function BaseChat({
if (sessionLoadError) {
return (
<div className="h-full flex flex-col min-h-0">
<MainPanelLayout
backgroundColor={'bg-background-primary'}
removeTopPadding={true}
{...customMainLayoutProps}
>
{renderHeader && renderHeader()}
<MainPanelLayout backgroundColor={'bg-background-primary'} removeTopPadding={true}>
<div className="flex flex-col flex-1 min-h-0 relative">
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center justify-center p-8">
@ -472,14 +455,7 @@ export default function BaseChat({
return (
<div className="h-full flex flex-col min-h-0">
<MainPanelLayout
backgroundColor={'bg-background-primary'}
removeTopPadding={true}
{...customMainLayoutProps}
>
{/* Custom header */}
{renderHeader && renderHeader()}
<MainPanelLayout backgroundColor={'bg-background-primary'} removeTopPadding={true}>
{/* Chat container with sticky recipe header */}
<div className="flex flex-col flex-1 min-h-0 relative">
{/* Goose watermark - top right */}
@ -521,7 +497,6 @@ export default function BaseChat({
<RecipeActivities
append={appendToChat}
activities={Array.isArray(recipe.activities) ? recipe.activities : null}
title={recipe.title}
/>
</div>
)}
@ -530,17 +505,17 @@ export default function BaseChat({
<>
<SearchView>
<div ref={conversationRef}>
<ProgressiveMessageList
messages={messages}
sessionId={sessionId}
toolCallNotifications={toolCallNotifications}
append={appendToChat}
isUserMessage={isUserMessage}
isStreamingMessage={chatState !== ChatState.Idle}
onRenderingComplete={handleRenderingComplete}
onMessageUpdate={onMessageUpdate}
submitElicitationResponse={submitElicitationResponse}
/>
<ProgressiveMessageList
messages={messages}
sessionId={sessionId}
toolCallNotifications={toolCallNotifications}
append={appendToChat}
isUserMessage={isUserMessage}
isStreamingMessage={chatState !== ChatState.Idle}
onRenderingComplete={handleRenderingComplete}
onMessageUpdate={onMessageUpdate}
submitElicitationResponse={submitElicitationResponse}
/>
</div>
</SearchView>
@ -636,7 +611,6 @@ export default function BaseChat({
}}
appendQuote={pendingQuote}
onAppendQuoteConsumed={() => setPendingQuote(null)}
{...customChatInputProps}
/>
</ChatInputCard>
</MainPanelLayout>

View file

@ -202,7 +202,7 @@ interface ChatInputProps {
recipeId?: string | null;
initialPrompt?: string;
append?: (message: Message) => void;
onWorkingDirChange?: (newDir: string) => Promise<void> | void;
onWorkingDirChange: (newDir: string) => Promise<void> | void;
inputRef?: React.RefObject<HTMLTextAreaElement | null>;
sessionModel?: string | null;
sessionProvider?: string | null;
@ -1746,7 +1746,7 @@ export default function ChatInput({
sessionId={sessionId ?? undefined}
workingDir={currentWorkingDir}
onWorkingDirChange={async (newDir) => {
await onWorkingDirChange?.(newDir);
await onWorkingDirChange(newDir);
setWorkingDirOverride(newDir);
}}
/>

View file

@ -29,7 +29,8 @@ const i18n = defineMessages({
},
blockedMessage: {
id: 'extensionInstallModal.blockedMessage',
defaultMessage: 'This extension command is not in the allowed list and its installation is blocked.\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator to request approval for this extension.',
defaultMessage:
'This extension command is not in the allowed list and its installation is blocked.\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator to request approval for this extension.',
},
ok: {
id: 'extensionInstallModal.ok',
@ -41,15 +42,18 @@ const i18n = defineMessages({
},
untrustedSecurityMessage: {
id: 'extensionInstallModal.untrustedSecurityMessage',
defaultMessage: 'This extension command is not in the allowed list and will be able to access your conversations and provide additional functionality.\n\nInstalling extensions from untrusted sources may pose security risks.',
defaultMessage:
'This extension command is not in the allowed list and will be able to access your conversations and provide additional functionality.\n\nInstalling extensions from untrusted sources may pose security risks.',
},
untrustedMessageWithUrl: {
id: 'extensionInstallModal.untrustedMessageWithUrl',
defaultMessage: '{securityMessage}\n\nExtension: {name}\nURL: {url}\n\nContact your administrator if you are unsure about this.',
defaultMessage:
'{securityMessage}\n\nExtension: {name}\nURL: {url}\n\nContact your administrator if you are unsure about this.',
},
untrustedMessageWithCommand: {
id: 'extensionInstallModal.untrustedMessageWithCommand',
defaultMessage: '{securityMessage}\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator if you are unsure about this.',
defaultMessage:
'{securityMessage}\n\nExtension: {name}\nCommand: {command}\n\nContact your administrator if you are unsure about this.',
},
installAnyway: {
id: 'extensionInstallModal.installAnyway',
@ -81,7 +85,8 @@ const i18n = defineMessages({
},
alreadyInstalledMessage: {
id: 'extensionInstallModal.alreadyInstalledMessage',
defaultMessage: "''{name}'' extension has already been installed successfully. Start a new chat session to use it.",
defaultMessage:
"''{name}'' extension has already been installed successfully. Start a new chat session to use it.",
},
installing: {
id: 'extensionInstallModal.installing',
@ -93,7 +98,7 @@ type ModalType = 'blocked' | 'untrusted' | 'trusted';
interface ExtensionInfo {
name: string;
command?: string;
command: string;
remoteUrl?: string;
link: string;
}
@ -116,7 +121,7 @@ interface ExtensionModalConfig {
}
interface ExtensionInstallModalProps {
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
addExtension: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
setView: (view: View, options?: ViewOptions) => void;
}
@ -200,7 +205,8 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
case 'blocked':
return {
title: intl.formatMessage(i18n.blockedTitle),
message: '\n\n' + intl.formatMessage(i18n.blockedMessage, { name, command: displayCommand }),
message:
'\n\n' + intl.formatMessage(i18n.blockedMessage, { name, command: displayCommand }),
confirmLabel: intl.formatMessage(i18n.ok),
cancelLabel: '',
showSingleButton: true,
@ -210,8 +216,16 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
case 'untrusted': {
const securityMessage = '\n\n' + intl.formatMessage(i18n.untrustedSecurityMessage);
const message = remoteUrl
? intl.formatMessage(i18n.untrustedMessageWithUrl, { securityMessage, name, url: remoteUrl })
: intl.formatMessage(i18n.untrustedMessageWithCommand, { securityMessage, name, command: displayCommand });
? intl.formatMessage(i18n.untrustedMessageWithUrl, {
securityMessage,
name,
url: remoteUrl,
})
: intl.formatMessage(i18n.untrustedMessageWithCommand, {
securityMessage,
name,
command: displayCommand,
});
return {
title: intl.formatMessage(i18n.untrustedTitle),
@ -236,58 +250,59 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
}
};
const handleExtensionRequest = useCallback(async (link: string): Promise<void> => {
if (processingLinkRef.current === link) {
return;
}
processingLinkRef.current = link;
try {
const command = extractCommand(link);
const remoteUrl = extractRemoteUrl(link);
const extName = extractExtensionName(link);
const extensionsList = await getExtensionsRef.current(true);
if (extensionsList?.find((ext) => ext.name === extName)) {
toastService.success({
title: intl.formatMessage(i18n.alreadyInstalledTitle, { name: extName }),
msg: intl.formatMessage(i18n.alreadyInstalledMessage, { name: extName }),
});
const handleExtensionRequest = useCallback(
async (link: string): Promise<void> => {
if (processingLinkRef.current === link) {
return;
}
processingLinkRef.current = link;
const extensionInfo: ExtensionInfo = {
name: extName,
command: command,
remoteUrl: remoteUrl || undefined,
link: link,
};
try {
const command = extractCommand(link);
const remoteUrl = extractRemoteUrl(link);
const extName = extractExtensionName(link);
const extensionsList = await getExtensionsRef.current(true);
const modalType = await determineModalType(command, remoteUrl);
if (extensionsList?.find((ext) => ext.name === extName)) {
toastService.success({
title: intl.formatMessage(i18n.alreadyInstalledTitle, { name: extName }),
msg: intl.formatMessage(i18n.alreadyInstalledMessage, { name: extName }),
});
return;
}
setModalState({
isOpen: true,
modalType,
extensionInfo,
isPending: false,
error: null,
});
const extensionInfo: ExtensionInfo = {
name: extName,
command: command,
remoteUrl: remoteUrl || undefined,
link: link,
};
setPendingLink(modalType === 'blocked' ? null : link);
const modalType = await determineModalType(command, remoteUrl);
window.electron.logInfo(`Extension modal opened: ${modalType} for ${extName}`);
} catch (error) {
console.error('Error processing extension request:', error);
setModalState((prev) => ({
...prev,
error: errorMessage(error, 'Unknown error'),
}));
} finally {
processingLinkRef.current = null;
}
}, [intl]);
setModalState({
isOpen: true,
modalType,
extensionInfo,
isPending: false,
error: null,
});
setPendingLink(modalType === 'blocked' ? null : link);
window.electron.logInfo(`Extension modal opened: ${modalType} for ${extName}`);
} catch (error) {
console.error('Error processing extension request:', error);
setModalState((prev) => ({
...prev,
error: errorMessage(error, 'Unknown error'),
}));
} finally {
processingLinkRef.current = null;
}
},
[intl]
);
const dismissModal = useCallback(() => {
setModalState({
@ -308,18 +323,13 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
setModalState((prev) => ({ ...prev, isPending: true }));
try {
if (addExtension) {
await addExtensionFromDeepLink(
pendingLink,
addExtension,
(view: string, options?: ViewOptions) => {
setView(view as View, options);
}
);
} else {
throw new Error('addExtension function not provided to component');
}
await addExtensionFromDeepLink(
pendingLink,
addExtension,
(view: string, options?: ViewOptions) => {
setView(view as View, options);
}
);
dismissModal();
} catch (error) {
setModalState((prev) => ({
@ -331,7 +341,6 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal
}, [pendingLink, dismissModal, addExtension, setView]);
useEffect(() => {
const handleAddExtension = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const link = args[0] as string;
await handleExtensionRequest(link);

View file

@ -28,22 +28,20 @@ const i18n = defineMessages({
});
interface ThemeSelectorProps {
className?: string;
hideTitle?: boolean;
horizontal?: boolean;
className: string;
hideTitle: boolean;
horizontal: boolean;
}
const ThemeSelector: React.FC<ThemeSelectorProps> = ({
className = '',
hideTitle = false,
horizontal = false,
}) => {
const ThemeSelector: React.FC<ThemeSelectorProps> = ({ className, hideTitle, horizontal }) => {
const intl = useIntl();
const { userThemePreference, setUserThemePreference } = useTheme();
return (
<div className={`${!horizontal ? 'px-1 py-2 space-y-2' : ''} ${className}`}>
{!hideTitle && <div className="text-xs text-text-primary px-3">{intl.formatMessage(i18n.theme)}</div>}
{!hideTitle && (
<div className="text-xs text-text-primary px-3">{intl.formatMessage(i18n.theme)}</div>
)}
<div
className={`${horizontal ? 'flex' : 'grid grid-cols-4'} gap-1 ${!horizontal ? 'px-3' : ''}`}
>

View file

@ -109,9 +109,6 @@ const NavRow: React.FC<NavRowProps> = ({ item, active, onClick }) => {
<button onClick={onClick} className={navItemClass(active)}>
<Icon className="w-5 h-5 flex-shrink-0 text-text-secondary" />
<span className="text-left flex-1 truncate">{getNavItemLabel(item, intl)}</span>
{item.getTag && (
<span className="text-xs font-mono text-text-secondary">{item.getTag()}</span>
)}
</button>
);
};
@ -242,15 +239,14 @@ const SessionRow: React.FC<SessionRowProps> = ({
};
export const Navigation: React.FC<{
className?: string;
activeLiveVoiceSessionId: string | null;
}> = ({ className, activeLiveVoiceSessionId }) => {
}> = ({ activeLiveVoiceSessionId }) => {
const intl = useIntl();
const { isNavExpanded } = useNavigationContext();
const location = useLocation();
const { extensionsList } = useConfig();
const appsExtensionEnabled = !!extensionsList?.find((ext) => ext.name === 'apps')?.enabled;
const appsExtensionEnabled = !!extensionsList.find((ext) => ext.name === 'apps')?.enabled;
const visibleItems = useMemo<NavItem[]>(() => {
return NAV_ITEMS.filter((item) => {
@ -338,7 +334,7 @@ export const Navigation: React.FC<{
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className={cn('bg-background-primary outline-none flex flex-col h-full', className)}
className="bg-background-primary outline-none flex flex-col h-full"
>
<div className="h-[48px] no-drag" />

View file

@ -18,7 +18,6 @@
import {
AppBridge,
PostMessageTransport,
type AppInfo,
type RequestHandlerExtra,
type SandboxConfig,
} from '@mcp-ui/client';
@ -177,7 +176,7 @@ interface GooseAppFrameProps {
toolInput?: Record<string, unknown>;
toolInputPartial?: Record<string, unknown>;
toolResult?: CallToolResult;
toolCancelled?: boolean;
toolCancelled: boolean;
onMessage: (params: {
content: Array<{ type: string; text?: string }>;
}) => Promise<Record<string, unknown>>;
@ -196,9 +195,8 @@ interface GooseAppFrameProps {
request: JSONRPCRequest,
extra: RequestHandlerExtra
) => Promise<Record<string, unknown>>;
onSizeChanged?: (params: McpUiSizeChangedNotification['params']) => void;
onInitialized?: (appInfo: AppInfo) => void;
onError?: (error: Error) => void;
onSizeChanged: (params: McpUiSizeChangedNotification['params']) => void;
onError: (error: Error) => void;
}
const SANDBOX_PROXY_READY_METHOD = 'ui/notifications/sandbox-proxy-ready';
@ -228,7 +226,6 @@ function GooseAppFrame({
onLoggingMessage,
onFallbackRequest,
onSizeChanged,
onInitialized,
onError,
}: GooseAppFrameProps) {
const containerRef = useRef<HTMLDivElement>(null);
@ -245,7 +242,6 @@ function GooseAppFrame({
const onLoggingMessageRef = useRef(onLoggingMessage);
const onFallbackRequestRef = useRef(onFallbackRequest);
const onSizeChangedRef = useRef(onSizeChanged);
const onInitializedRef = useRef(onInitialized);
const onErrorRef = useRef(onError);
useEffect(() => {
@ -257,7 +253,6 @@ function GooseAppFrame({
onLoggingMessageRef.current = onLoggingMessage;
onFallbackRequestRef.current = onFallbackRequest;
onSizeChangedRef.current = onSizeChanged;
onInitializedRef.current = onInitialized;
onErrorRef.current = onError;
});
@ -305,7 +300,7 @@ function GooseAppFrame({
settled = true;
cleanupReadyListener();
if (active) {
onErrorRef.current?.(error);
onErrorRef.current(error);
}
};
const ready = () => {
@ -330,7 +325,7 @@ function GooseAppFrame({
if (!active || !iframe.contentWindow) return;
try {
bridge.onsizechange = (params) => {
onSizeChangedRef.current?.(params);
onSizeChangedRef.current(params);
if (params.width !== undefined) {
iframe.style.width = `${params.width}px`;
}
@ -342,10 +337,6 @@ function GooseAppFrame({
bridge.oninitialized = () => {
if (!active) return;
setInitialized(true);
onInitializedRef.current?.({
appVersion: bridge.getAppVersion(),
appCapabilities: bridge.getAppCapabilities(),
});
};
await bridge.connect(new PostMessageTransport(iframe.contentWindow, iframe.contentWindow));
if (!active) return;
@ -353,7 +344,7 @@ function GooseAppFrame({
setConnected(true);
} catch (error) {
if (!active) return;
onErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
onErrorRef.current(error instanceof Error ? error : new Error(String(error)));
}
};
@ -382,7 +373,7 @@ function GooseAppFrame({
if (!connected || !bridge) return;
void Promise.resolve(bridge.sendSandboxResourceReady({ html, csp: sandbox.csp })).catch(
(error: unknown) => {
onErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
onErrorRef.current(error instanceof Error ? error : new Error(String(error)));
}
);
}, [connected, html, sandbox.csp]);

View file

@ -315,18 +315,16 @@ export default function ToolCallWithResponse({
interface ToolCallExpandableProps {
label: string | React.ReactNode;
isStartExpanded?: boolean;
isStartExpanded: boolean;
isForceExpand?: boolean;
children: React.ReactNode;
className?: string;
}
function ToolCallExpandable({
label,
isStartExpanded = false,
isStartExpanded,
isForceExpand,
children,
className = '',
}: ToolCallExpandableProps) {
const [isExpandedState, setIsExpanded] = React.useState<boolean | null>(null);
const isExpanded = isExpandedState === null ? isStartExpanded : isExpandedState;
@ -336,7 +334,7 @@ function ToolCallExpandable({
}, [isForceExpand]);
return (
<div className={className}>
<div>
<Button
onClick={toggleExpand}
className="group w-full flex justify-between items-center pr-2 transition-colors rounded-none"
@ -1082,7 +1080,7 @@ function ToolLogsView({
}: {
logs: string[];
working: boolean;
isStartExpanded?: boolean;
isStartExpanded: boolean;
}) {
const intl = useIntl();
const boxRef = useRef<HTMLDivElement>(null);

View file

@ -16,7 +16,6 @@ const alertIcons: Record<AlertType, React.ReactNode> = {
interface AlertBoxProps {
alert: Alert;
className?: string;
compactButtonEnabled?: boolean;
}
const i18n = defineMessages({

View file

@ -81,9 +81,7 @@ interface DirSwitcherProps {
className: string;
sessionId: string | undefined;
workingDir: string;
onWorkingDirChange?: (newDir: string) => Promise<void> | void;
onRestartStart?: () => void;
onRestartEnd?: () => void;
onWorkingDirChange: (newDir: string) => Promise<void> | void;
}
export const DirSwitcher: React.FC<DirSwitcherProps> = ({
@ -91,8 +89,6 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
sessionId,
workingDir,
onWorkingDirChange,
onRestartStart,
onRestartEnd,
}) => {
const intl = useIntl();
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
@ -129,19 +125,15 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
const applyDirectoryChange = async (newDir: string) => {
if (sessionId) {
onRestartStart?.();
try {
await onWorkingDirChange?.(newDir);
await onWorkingDirChange(newDir);
} catch (error) {
console.error('[DirSwitcher] Failed to update working directory:', error);
toast.error(intl.formatMessage(i18n.failedToUpdateWorkingDir));
return;
} finally {
onRestartEnd?.();
}
} else {
await onWorkingDirChange?.(newDir);
await onWorkingDirChange(newDir);
}
// Only record the directory after the backend confirmed the change, so a

View file

@ -30,8 +30,8 @@ interface InlineEditTextProps {
disabled?: boolean;
className?: string;
editClassName?: string;
onEditStart?: () => void;
onEditEnd?: () => void;
onEditStart: () => void;
onEditEnd: () => void;
allowEmpty?: boolean;
singleClickEdit?: boolean;
}
@ -75,13 +75,13 @@ export const InlineEditText: React.FC<InlineEditTextProps> = ({
if (disabled || isSaving) return;
setIsEditing(true);
setEditValue(value);
onEditStart?.();
onEditStart();
}, [disabled, isSaving, value, onEditStart]);
const handleCancel = useCallback(() => {
setIsEditing(false);
setEditValue(originalValue.current);
onEditEnd?.();
onEditEnd();
}, [onEditEnd]);
const handleSave = useCallback(async () => {
@ -106,7 +106,7 @@ export const InlineEditText: React.FC<InlineEditTextProps> = ({
await onSave(trimmedValue);
originalValue.current = trimmedValue;
setIsEditing(false);
onEditEnd?.();
onEditEnd();
} catch (error) {
const errMsg = errorMessage(error, intl.formatMessage(i18n.failedToSave));
console.error('InlineEditText save error:', errMsg);
@ -197,7 +197,13 @@ export const InlineEditText: React.FC<InlineEditTextProps> = ({
`}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
title={disabled ? '' : singleClickEdit ? intl.formatMessage(i18n.clickToEdit) : intl.formatMessage(i18n.doubleClickToEdit)}
title={
disabled
? ''
: singleClickEdit
? intl.formatMessage(i18n.clickToEdit)
: intl.formatMessage(i18n.doubleClickToEdit)
}
>
{value || <span className="text-text-subtle italic">{resolvedPlaceholder}</span>}
</div>

View file

@ -36,8 +36,8 @@ interface SearchBarProps {
onSearch: (term: string, caseSensitive: boolean) => void;
/** Callback fired when the search bar is closed */
onClose: () => void;
/** Optional callback for navigating between search results */
onNavigate?: (direction: 'next' | 'prev') => void;
/** Callback for navigating between search results */
onNavigate: (direction: 'next' | 'prev') => void;
/** Current search results state */
searchResults?: {
count: number;
@ -153,7 +153,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
e?.preventDefault();
if (searchResults && searchResults.count > 0) {
inputRef.current?.focus();
onNavigate?.(direction);
onNavigate(direction);
}
};

View file

@ -86,18 +86,18 @@ const i18n = defineMessages({
interface ParameterInputProps {
parameter: Parameter;
onChange: (name: string, updatedParameter: Partial<Parameter>) => void;
onDelete?: (parameterKey: string) => void;
isUnused?: boolean;
isExpanded?: boolean;
onToggleExpanded?: (parameterKey: string) => void;
onDelete: (parameterKey: string) => void;
isUnused: boolean;
isExpanded: boolean;
onToggleExpanded: (parameterKey: string) => void;
}
const ParameterInput: React.FC<ParameterInputProps> = ({
parameter,
onChange,
onDelete,
isUnused = false,
isExpanded = true,
isUnused,
isExpanded,
onToggleExpanded,
}) => {
const intl = useIntl();
@ -106,7 +106,7 @@ const ParameterInput: React.FC<ParameterInputProps> = ({
const handleToggleExpanded = (e: React.MouseEvent) => {
// Only toggle if we're not clicking on the delete button
if (onToggleExpanded && !(e.target as HTMLElement).closest('button')) {
if (!(e.target as HTMLElement).closest('button')) {
onToggleExpanded(key);
}
};
@ -115,26 +115,24 @@ const ParameterInput: React.FC<ParameterInputProps> = ({
<div className="parameter-input my-4 border rounded-lg bg-background-secondary shadow-sm relative">
{/* Collapsed header - always visible */}
<div
className={`flex items-center justify-between p-4 ${onToggleExpanded ? 'cursor-pointer hover:bg-background-primary/50' : ''} transition-colors`}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-background-primary/50 transition-colors"
onClick={handleToggleExpanded}
>
<div className="flex items-center gap-2 flex-1">
{onToggleExpanded && (
<button
type="button"
className="p-1 hover:bg-background-primary rounded transition-colors"
onClick={(e) => {
e.stopPropagation();
onToggleExpanded(key);
}}
>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-text-secondary" />
) : (
<ChevronRight className="w-4 h-4 text-text-secondary" />
)}
</button>
)}
<button
type="button"
className="p-1 hover:bg-background-primary rounded transition-colors"
onClick={(e) => {
e.stopPropagation();
onToggleExpanded(key);
}}
>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-text-secondary" />
) : (
<ChevronRight className="w-4 h-4 text-text-secondary" />
)}
</button>
<div className="flex items-center gap-2">
<span className="text-md font-bold text-text-primary">
@ -146,25 +144,25 @@ const ParameterInput: React.FC<ParameterInputProps> = ({
title={intl.formatMessage(i18n.unusedWarningTitle)}
>
<AlertTriangle className="w-4 h-4 text-orange-500" />
<span className="text-xs text-orange-500 font-normal">{intl.formatMessage(i18n.unused)}</span>
<span className="text-xs text-orange-500 font-normal">
{intl.formatMessage(i18n.unused)}
</span>
</div>
)}
</div>
</div>
{onDelete && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onDelete(key);
}}
className="p-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded transition-colors"
title={intl.formatMessage(i18n.deleteParameter, { key })}
>
<Trash2 className="w-4 h-4" />
</button>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onDelete(key);
}}
className="p-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded transition-colors"
title={intl.formatMessage(i18n.deleteParameter, { key })}
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{/* Expandable content - only shown when expanded */}

View file

@ -113,7 +113,6 @@ interface CreateEditRecipeModalProps {
recipe?: Recipe;
isCreateMode?: boolean;
recipeId?: string | null;
onRecipeSaved?: (savedRecipeId: string) => void;
}
export default function CreateEditRecipeModal({
@ -122,7 +121,6 @@ export default function CreateEditRecipeModal({
recipe,
isCreateMode = false,
recipeId,
onRecipeSaved,
}: CreateEditRecipeModalProps) {
const intl = useIntl();
const getInitialValues = React.useCallback((): RecipeFormData => {
@ -423,11 +421,7 @@ export default function CreateEditRecipeModal({
try {
const recipe = getCurrentRecipe();
const { id: savedRecipeId } = await saveRecipe(recipe, recipeId);
if (onRecipeSaved) {
onRecipeSaved(savedRecipeId);
}
await saveRecipe(recipe, recipeId);
onClose(true);

View file

@ -5,7 +5,6 @@ import MarkdownContent from '../MarkdownContent';
interface RecipeActivitiesProps {
append: (text: string) => void;
activities: string[] | null;
title?: string;
}
export default function RecipeActivities({ append, activities }: RecipeActivitiesProps) {

View file

@ -129,14 +129,14 @@ interface CreateSubRecipeInlineProps {
isOpen: boolean;
onClose: () => void;
onSubRecipeSaved: (subRecipe: SubRecipeFormData) => void;
existingSubRecipes?: SubRecipeFormData[];
existingSubRecipes: SubRecipeFormData[];
}
export default function CreateSubRecipeInline({
isOpen,
onClose,
onSubRecipeSaved,
existingSubRecipes = [],
existingSubRecipes,
}: CreateSubRecipeInlineProps) {
const intl = useIntl();
useEscapeKey(isOpen, onClose);
@ -221,12 +221,24 @@ export default function CreateSubRecipeInline({
toastError({
title: intl.formatMessage(i18n.saveFailed),
msg: intl.formatMessage(i18n.saveFailedMsg, { error: error instanceof Error ? error.message : 'Unknown error' }),
msg: intl.formatMessage(i18n.saveFailedMsg, {
error: error instanceof Error ? error.message : 'Unknown error',
}),
});
} finally {
setIsSaving(false);
}
}, [form, name, toolDescription, sequentialWhenRepeated, values, existingSubRecipes, onSubRecipeSaved, onClose, intl]);
}, [
form,
name,
toolDescription,
sequentialWhenRepeated,
values,
existingSubRecipes,
onSubRecipeSaved,
onClose,
intl,
]);
if (!isOpen) return null;
@ -236,10 +248,10 @@ export default function CreateSubRecipeInline({
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-borderSubtle">
<div>
<h2 className="text-xl font-medium text-textProminent">{intl.formatMessage(i18n.title)}</h2>
<p className="text-textSubtle text-sm">
{intl.formatMessage(i18n.subtitle)}
</p>
<h2 className="text-xl font-medium text-textProminent">
{intl.formatMessage(i18n.title)}
</h2>
<p className="text-textSubtle text-sm">{intl.formatMessage(i18n.subtitle)}</p>
</div>
<Button
onClick={onClose}
@ -270,9 +282,7 @@ export default function CreateSubRecipeInline({
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
placeholder={intl.formatMessage(i18n.namePlaceholder)}
/>
<p className="text-xs text-text-muted mt-1">
{intl.formatMessage(i18n.nameHint)}
</p>
<p className="text-xs text-text-muted mt-1">{intl.formatMessage(i18n.nameHint)}</p>
</div>
{/* Title Field */}
@ -283,7 +293,8 @@ export default function CreateSubRecipeInline({
htmlFor="subrecipe-title"
className="block text-sm font-medium text-text-standard mb-2"
>
{intl.formatMessage(i18n.recipeTitleLabel)} <span className="text-text-danger">*</span>
{intl.formatMessage(i18n.recipeTitleLabel)}{' '}
<span className="text-text-danger">*</span>
</label>
<input
id="subrecipe-title"
@ -306,7 +317,8 @@ export default function CreateSubRecipeInline({
htmlFor="recipe-description"
className="block text-sm font-medium text-text-standard mb-2"
>
{intl.formatMessage(i18n.recipeDescriptionLabel)} <span className="text-text-danger">*</span>
{intl.formatMessage(i18n.recipeDescriptionLabel)}{' '}
<span className="text-text-danger">*</span>
</label>
<input
id="recipe-description"
@ -329,7 +341,8 @@ export default function CreateSubRecipeInline({
htmlFor="subrecipe-instructions"
className="block text-sm font-medium text-text-standard mb-2"
>
{intl.formatMessage(i18n.instructionsLabel)} <span className="text-text-danger">*</span>
{intl.formatMessage(i18n.instructionsLabel)}{' '}
<span className="text-text-danger">*</span>
</label>
<textarea
id="subrecipe-instructions"

View file

@ -104,15 +104,7 @@ import { RecipeExtensionSelector } from './RecipeExtensionSelector';
type FormFieldApi<_T = any> = any;
interface RecipeFormFieldsProps {
// Form instance from parent
form: RecipeFormApi;
// Event handlers
onTitleChange?: (value: string) => void;
onDescriptionChange?: (value: string) => void;
onInstructionsChange?: (value: string) => void;
onPromptChange?: (value: string) => void;
onJsonSchemaChange?: (value: string) => void;
}
const BUILT_IN_RECIPE_DIR_PARAM = 'recipe_dir';
@ -138,14 +130,7 @@ export const extractTemplateVariables = (content: string): string[] => {
return variables;
};
export function RecipeFormFields({
form,
onTitleChange,
onDescriptionChange,
onInstructionsChange,
onPromptChange,
onJsonSchemaChange,
}: RecipeFormFieldsProps) {
export function RecipeFormFields({ form }: RecipeFormFieldsProps) {
const intl = useIntl();
const [showJsonSchemaEditor, setShowJsonSchemaEditor] = useState(false);
const [showInstructionsEditor, setShowInstructionsEditor] = useState(false);
@ -269,10 +254,7 @@ export function RecipeFormFields({
id="recipe-title"
type="text"
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value);
onTitleChange?.(e.target.value);
}}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className={`w-full p-3 border rounded-lg bg-background-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-primary'
@ -301,10 +283,7 @@ export function RecipeFormFields({
id="recipe-description"
type="text"
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value);
onDescriptionChange?.(e.target.value);
}}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className={`w-full p-3 border rounded-lg bg-background-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-primary'
@ -343,10 +322,7 @@ export function RecipeFormFields({
<textarea
id="recipe-instructions"
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value);
onInstructionsChange?.(e.target.value);
}}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={() => {
field.handleBlur();
updateParametersFromFields();
@ -372,7 +348,6 @@ export function RecipeFormFields({
value={field.state.value}
onChange={(value) => {
field.handleChange(value);
onInstructionsChange?.(value);
updateParametersFromFields();
}}
error={field.state.meta.errors.length > 0 ? field.state.meta.errors[0] : undefined}
@ -397,10 +372,7 @@ export function RecipeFormFields({
<textarea
id="recipe-prompt"
value={field.state.value || ''}
onChange={(e) => {
field.handleChange(e.target.value);
onPromptChange?.(e.target.value);
}}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={() => {
field.handleBlur();
updateParametersFromFields();
@ -636,10 +608,7 @@ export function RecipeFormFields({
isOpen={showJsonSchemaEditor}
onClose={() => setShowJsonSchemaEditor(false)}
value={field.state.value || ''}
onChange={(value) => {
field.handleChange(value);
onJsonSchemaChange?.(value);
}}
onChange={(value) => field.handleChange(value)}
error={
field.state.meta.errors.length > 0 ? field.state.meta.errors[0] : undefined
}

View file

@ -79,10 +79,6 @@ const i18n = defineMessages({
inspectError: { id: 'schedulesView.inspectError', defaultMessage: 'Inspect Job Error' },
});
interface SchedulesViewProps {
onClose?: () => void;
}
const ScheduleCard: React.FC<{
job: ScheduledJobDto;
onNavigateToDetail: (id: string) => void;
@ -238,7 +234,7 @@ const ScheduleCard: React.FC<{
);
};
const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
const SchedulesView = () => {
const intl = useIntl();
const location = useLocation();
const [schedules, setSchedules] = useState<ScheduledJobDto[]>([]);

View file

@ -146,11 +146,10 @@ interface EditSessionModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (sessionId: string, newDescription: string) => Promise<void>;
disabled?: boolean;
}
const EditSessionModal = React.memo<EditSessionModalProps>(
({ session, isOpen, onClose, onSave, disabled = false }) => {
({ session, isOpen, onClose, onSave }) => {
const intl = useIntl();
const [description, setDescription] = useState('');
const [isUpdating, setIsUpdating] = useState(false);
@ -165,7 +164,7 @@ const EditSessionModal = React.memo<EditSessionModalProps>(
}, [session, isOpen]);
const handleSave = useCallback(async () => {
if (!session || disabled) return;
if (!session) return;
const trimmedDescription = description.trim();
if (trimmedDescription === session.name) {
@ -189,7 +188,7 @@ const EditSessionModal = React.memo<EditSessionModalProps>(
} finally {
setIsUpdating(false);
}
}, [session, description, onSave, onClose, disabled, intl]);
}, [session, description, onSave, onClose, intl]);
const handleCancel = useCallback(() => {
if (!isUpdating) {
@ -233,18 +232,18 @@ const EditSessionModal = React.memo<EditSessionModalProps>(
autoFocus
maxLength={200}
onKeyDown={handleKeyDown}
disabled={isUpdating || disabled}
disabled={isUpdating}
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<Button onClick={handleCancel} variant="ghost" disabled={isUpdating || disabled}>
<Button onClick={handleCancel} variant="ghost" disabled={isUpdating}>
{intl.formatMessage(i18n.cancel)}
</Button>
<Button
onClick={handleSave}
disabled={!description.trim() || isUpdating || disabled}
disabled={!description.trim() || isUpdating}
variant="default"
>
{isUpdating ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.save)}
@ -810,7 +809,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
);
});
const SessionSkeleton = React.memo(({ variant = 0 }: { variant?: number }) => {
const SessionSkeleton = React.memo(({ variant }: { variant: number }) => {
const titleWidths = ['w-3/4', 'w-2/3', 'w-4/5', 'w-1/2'];
const pathWidths = ['w-32', 'w-28', 'w-36', 'w-24'];
const tokenWidths = ['w-12', 'w-10', 'w-14', 'w-8'];
@ -909,52 +908,54 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
visibleGroupsCount >= activeDateGroups.length &&
memoizedScheduledDateGroups.length > 0 && (
<div className="space-y-4">
<button
onClick={() => setIsScheduledExpanded((v) => !v)}
aria-expanded={isScheduledExpanded}
aria-controls="scheduled-job-sessions"
className="sticky top-0 z-10 w-full flex items-center justify-between bg-background-primary/95 backdrop-blur-sm py-2 px-1 rounded-lg hover:bg-background-secondary transition-colors cursor-pointer"
>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-text-secondary" />
<h2 className="text-text-secondary font-medium">
{intl.formatMessage(i18n.scheduledJobs)}
</h2>
<span className="text-xs text-text-tertiary bg-background-secondary px-2 py-0.5 rounded-full">
{intl.formatMessage(i18n.scheduledJobsCount, { count: scheduledSessions.length })}
</span>
</div>
{isScheduledExpanded ? (
<ChevronDown className="w-4 h-4 text-text-secondary" />
) : (
<ChevronRight className="w-4 h-4 text-text-secondary" />
)}
</button>
<button
onClick={() => setIsScheduledExpanded((v) => !v)}
aria-expanded={isScheduledExpanded}
aria-controls="scheduled-job-sessions"
className="sticky top-0 z-10 w-full flex items-center justify-between bg-background-primary/95 backdrop-blur-sm py-2 px-1 rounded-lg hover:bg-background-secondary transition-colors cursor-pointer"
>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-text-secondary" />
<h2 className="text-text-secondary font-medium">
{intl.formatMessage(i18n.scheduledJobs)}
</h2>
<span className="text-xs text-text-tertiary bg-background-secondary px-2 py-0.5 rounded-full">
{intl.formatMessage(i18n.scheduledJobsCount, {
count: scheduledSessions.length,
})}
</span>
</div>
{isScheduledExpanded ? (
<ChevronDown className="w-4 h-4 text-text-secondary" />
) : (
<ChevronRight className="w-4 h-4 text-text-secondary" />
)}
</button>
{isScheduledExpanded && (
<div id="scheduled-job-sessions" className="space-y-8">
{memoizedScheduledDateGroups.map((group) => (
<div key={group.label} className="space-y-4">
<div className="sticky top-0 z-10 bg-background-primary/95 backdrop-blur-sm">
<h2 className="text-text-secondary">{group.label}</h2>
{isScheduledExpanded && (
<div id="scheduled-job-sessions" className="space-y-8">
{memoizedScheduledDateGroups.map((group) => (
<div key={group.label} className="space-y-4">
<div className="sticky top-0 z-10 bg-background-primary/95 backdrop-blur-sm">
<h2 className="text-text-secondary">{group.label}</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{group.sessions.map((session) => (
<SessionItem
key={session.id}
session={session}
onEditClick={handleEditSession}
onDuplicateClick={handleDuplicateSession}
onDeleteClick={handleDeleteSession}
onExportClick={handleExportSession}
onOpenInNewWindow={handleOpenInNewWindow}
/>
))}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{group.sessions.map((session) => (
<SessionItem
key={session.id}
session={session}
onEditClick={handleEditSession}
onDuplicateClick={handleDuplicateSession}
onDeleteClick={handleDeleteSession}
onExportClick={handleExportSession}
onOpenInNewWindow={handleOpenInNewWindow}
/>
))}
</div>
</div>
))}
</div>
)}
))}
</div>
)}
</div>
)}

View file

@ -43,12 +43,11 @@ const i18n = defineMessages({
interface ExtensionSectionProps {
deepLinkConfig?: ExtensionConfig;
showEnvVars?: boolean;
hideButtons?: boolean;
hideButtons: boolean;
disableConfiguration?: boolean;
customToggle?: (extension: FixedExtensionEntry) => Promise<boolean | void>;
selectedExtensions?: string[]; // Add controlled state
onModalClose?: (extensionName: string) => void;
searchTerm?: string;
onModalClose: (extensionName: string) => void;
searchTerm: string;
}
export default function ExtensionsSection({
@ -56,10 +55,9 @@ export default function ExtensionsSection({
showEnvVars,
hideButtons,
disableConfiguration,
customToggle,
selectedExtensions = [],
onModalClose,
searchTerm = '',
searchTerm,
}: ExtensionSectionProps) {
const intl = useIntl();
const { getExtensions, addExtension, removeExtension, setExtensionEnabled, extensionsList } =
@ -109,11 +107,6 @@ export default function ExtensionsSection({
}, [getExtensions]);
const handleExtensionToggle = async (extensionConfig: FixedExtensionEntry) => {
if (customToggle) {
await customToggle(extensionConfig);
return true;
}
const toggleDirection = extensionConfig.enabled ? 'toggleOff' : 'toggleOn';
const configKey = extensionConfig.configKey ?? nameToKey(extensionConfig.name);
@ -146,11 +139,9 @@ export default function ExtensionsSection({
console.error('Failed to add extension:', error);
} finally {
await fetchExtensions();
if (onModalClose) {
setTimeout(() => {
onModalClose(formData.name);
}, 200);
}
setTimeout(() => {
onModalClose(formData.name);
}, 200);
}
};

View file

@ -33,8 +33,8 @@ interface ExtensionConfigFieldsProps {
full_cmd: string;
endpoint: string;
onChange: (key: string, value: string) => void;
submitAttempted?: boolean;
isValid?: boolean;
submitAttempted: boolean;
isValid: boolean;
}
export default function ExtensionConfigFields({
@ -42,7 +42,7 @@ export default function ExtensionConfigFields({
full_cmd,
endpoint,
onChange,
submitAttempted = false,
submitAttempted,
isValid,
}: ExtensionConfigFieldsProps) {
const intl = useIntl();

View file

@ -19,7 +19,9 @@ describe('ExtensionItem', () => {
it('reflects the toggle as OFF immediately when disabling, before the async toggle resolves', async () => {
// onToggle stays pending so we observe the in-flight (optimistic) state
const onToggle = vi.fn(() => new Promise<void>(() => {}));
renderWithIntl(<ExtensionItem extension={makeExtension(true)} onToggle={onToggle} />);
renderWithIntl(
<ExtensionItem extension={makeExtension(true)} onToggle={onToggle} onConfigure={vi.fn()} />
);
const toggle = screen.getByRole('switch');
expect(toggle).toHaveAttribute('aria-checked', 'true');
@ -33,7 +35,9 @@ describe('ExtensionItem', () => {
it('reflects the toggle as ON immediately when enabling, before the async toggle resolves', async () => {
const onToggle = vi.fn(() => new Promise<void>(() => {}));
renderWithIntl(<ExtensionItem extension={makeExtension(false)} onToggle={onToggle} />);
renderWithIntl(
<ExtensionItem extension={makeExtension(false)} onToggle={onToggle} onConfigure={vi.fn()} />
);
const toggle = screen.getByRole('switch');
expect(toggle).toHaveAttribute('aria-checked', 'false');

View file

@ -21,7 +21,7 @@ const i18n = defineMessages({
interface ExtensionItemProps {
extension: FixedExtensionEntry;
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void;
onConfigure?: (extension: FixedExtensionEntry) => void;
onConfigure: (extension: FixedExtensionEntry) => void;
isStatic?: boolean; // to not allow users to edit configuration
}
@ -100,7 +100,7 @@ export default function ExtensionItem({
aria-label={intl.formatMessage(i18n.configureExtension, {
name: getFriendlyTitle(extension),
})}
onClick={() => onConfigure?.(extension)}
onClick={() => onConfigure(extension)}
>
<Gear className="w-4 h-4" />
</button>

View file

@ -27,10 +27,10 @@ const i18n = defineMessages({
interface ExtensionListProps {
extensions: FixedExtensionEntry[];
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void;
onConfigure?: (extension: FixedExtensionEntry) => void;
onConfigure: (extension: FixedExtensionEntry) => void;
isStatic?: boolean;
disableConfiguration?: boolean;
searchTerm?: string;
searchTerm: string;
}
export default function ExtensionList({
@ -39,7 +39,7 @@ export default function ExtensionList({
onConfigure,
isStatic,
disableConfiguration: _disableConfiguration,
searchTerm = '',
searchTerm,
}: ExtensionListProps) {
const matchesSearch = (extension: FixedExtensionEntry): boolean => {
if (!searchTerm) return true;

View file

@ -97,13 +97,11 @@ function ProviderCards({
isOnboarding,
refreshProviders,
setView,
onModelSelected,
}: {
providers: ProviderDetails[];
isOnboarding: boolean;
refreshProviders?: () => void;
setView?: (view: View) => void;
onModelSelected?: (model?: string) => void;
refreshProviders: () => void;
setView: (view: View) => void;
}) {
const intl = useIntl();
const [searchQuery, setSearchQuery] = useState('');
@ -168,9 +166,7 @@ function ProviderCards({
const providerId = editingProvider.id;
setShowCustomProviderModal(false);
setEditingProvider(null);
if (refreshProviders) {
refreshProviders();
}
refreshProviders();
setSwitchModelProvider(providerId);
setShowSwitchModelModal(true);
},
@ -184,9 +180,7 @@ function ProviderCards({
setShowCustomProviderModal(false);
setEditingProvider(null);
setIsActiveProvider(false);
if (refreshProviders) {
refreshProviders();
}
refreshProviders();
}, [editingProvider, refreshProviders]);
const handleCloseModal = useCallback(() => {
@ -197,17 +191,13 @@ function ProviderCards({
const onCloseProviderConfig = useCallback(() => {
setConfiguringProvider(null);
if (refreshProviders) {
refreshProviders();
}
refreshProviders();
}, [refreshProviders]);
const onProviderConfigured = useCallback(
async (provider: ProviderDetails) => {
setConfiguringProvider(null);
if (refreshProviders) {
await refreshProviders();
}
await refreshProviders();
setSwitchModelProvider(provider.name);
setShowSwitchModelModal(true);
},
@ -221,9 +211,7 @@ function ProviderCards({
const handleSetView = useCallback(
(view: View) => {
setShowSwitchModelModal(false);
if (setView) {
setView(view);
}
setView(view);
},
[setView]
);
@ -233,9 +221,7 @@ function ProviderCards({
const result = await acpCreateCustomProviderFromRequest(data);
const providerId = result.provider_name;
setShowCustomProviderModal(false);
if (refreshProviders) {
await refreshProviders();
}
await refreshProviders();
setSwitchModelProvider(providerId);
setShowSwitchModelModal(true);
},
@ -295,7 +281,7 @@ function ProviderCards({
api_key: '',
models: editingProvider.config.models ?? [],
supports_streaming: editingProvider.config.supportsStreaming ?? true,
requires_auth: editingProvider.config.requiresAuth ?? true,
requires_auth: editingProvider.config.requiresAuth,
headers: editingProvider.config.headers ?? undefined,
catalog_provider_id: editingProvider.config.catalogProviderId ?? undefined,
toolshim: editingProvider.config.toolshim,
@ -357,7 +343,6 @@ function ProviderCards({
sessionId={null}
onClose={onCloseSwitchModelModal}
setView={handleSetView}
onModelSelected={onModelSelected}
initialProvider={switchModelProvider}
titleOverride={intl.formatMessage(i18n.chooseModel)}
/>
@ -371,13 +356,11 @@ export default function ProviderGrid({
isOnboarding,
refreshProviders,
setView,
onModelSelected,
}: {
providers: ProviderDetails[];
isOnboarding: boolean;
refreshProviders?: () => void;
setView?: (view: View) => void;
onModelSelected?: (model?: string) => void;
refreshProviders: () => void;
setView: (view: View) => void;
}) {
return (
<ProviderCards
@ -385,7 +368,6 @@ export default function ProviderGrid({
isOnboarding={isOnboarding}
refreshProviders={refreshProviders}
setView={setView}
onModelSelected={onModelSelected}
/>
);
}

View file

@ -34,14 +34,9 @@ const i18n = defineMessages({
interface ProviderSettingsProps {
onClose: () => void;
isOnboarding: boolean;
onProviderLaunched?: (model?: string) => void;
}
export default function ProviderSettings({
onClose,
isOnboarding,
onProviderLaunched,
}: ProviderSettingsProps) {
export default function ProviderSettings({ onClose, isOnboarding }: ProviderSettingsProps) {
const intl = useIntl();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
@ -118,7 +113,6 @@ export default function ProviderSettings({
isOnboarding={isOnboarding}
refreshProviders={refreshProviders}
setView={setView}
onModelSelected={onProviderLaunched}
/>
)}
</div>

View file

@ -1,5 +1,4 @@
import { useState, useEffect } from 'react';
import { Button } from '../../../../ui/button';
import { Search, ExternalLink, Check } from 'lucide-react';
import { Input } from '../../../../ui/input';
import { Select } from '../../../../ui/Select';
@ -59,23 +58,13 @@ const i18n = defineMessages({
id: 'providerCatalogPicker.requiresEnvVar',
defaultMessage: ' • Requires {envVar}',
},
cancel: {
id: 'providerCatalogPicker.cancel',
defaultMessage: 'Cancel',
},
});
interface ProviderCatalogPickerProps {
onSelect: (template: ProviderTemplateDto) => void;
onCancel: () => void;
embedded?: boolean;
}
export default function ProviderCatalogPicker({
onSelect,
onCancel,
embedded,
}: ProviderCatalogPickerProps) {
export default function ProviderCatalogPicker({ onSelect }: ProviderCatalogPickerProps) {
const intl = useIntl();
const [selectedFormat, setSelectedFormat] = useState<string>('openai');
const [providers, setProviders] = useState<ProviderTemplateCatalogEntryDto[]>([]);
@ -237,15 +226,6 @@ export default function ProviderCatalogPicker({
)}
</div>
)}
{/* Actions */}
{!embedded && (
<div className="flex justify-end space-x-2 pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
{intl.formatMessage(i18n.cancel)}
</Button>
</div>
)}
</div>
);
}

View file

@ -44,14 +44,14 @@ const i18n = defineMessages({
interface ProviderSetupActionsProps {
onCancel: () => void;
onSubmit: (e: SyntheticEvent) => void;
onDelete?: () => void;
showDeleteConfirmation?: boolean;
onConfirmDelete?: () => void;
onCancelDelete?: () => void;
canDelete?: boolean;
providerName?: string;
primaryParameters?: ConfigKey[];
isActiveProvider?: boolean; // Made optional with default false
onDelete: () => void;
showDeleteConfirmation: boolean;
onConfirmDelete: () => void;
onCancelDelete: () => void;
canDelete: boolean;
providerName: string;
primaryParameters: ConfigKey[];
isActiveProvider: boolean;
}
/**
@ -68,7 +68,7 @@ export default function ProviderSetupActions({
canDelete,
providerName,
primaryParameters,
isActiveProvider = false, // Default value provided
isActiveProvider,
}: ProviderSetupActionsProps) {
const intl = useIntl();

View file

@ -251,7 +251,7 @@ interface CustomProviderFormProps {
onDelete?: () => Promise<void>;
isActiveProvider?: boolean;
initialData: UpdateCustomProviderRequest | null;
isEditable?: boolean;
isEditable: boolean;
}
export default function CustomProviderForm({
@ -315,7 +315,7 @@ export default function CustomProviderForm({
setModels(initialData.models.join(', '));
setSupportsStreaming(initialData.supports_streaming ?? true);
setToolshim(initialData.toolshim);
setRequiresAuth(initialData.requires_auth ?? true);
setRequiresAuth(initialData.requires_auth);
if (initialData.headers) {
const headerList = Object.entries(initialData.headers).map(([key, value]) => ({
@ -453,7 +453,7 @@ export default function CustomProviderForm({
const errors: Record<string, string> = {};
if (!displayName) errors.displayName = intl.formatMessage(i18n.displayNameRequired);
if (!apiUrl) errors.apiUrl = intl.formatMessage(i18n.apiUrlRequired);
const existingHadAuth = initialData && (initialData.requires_auth ?? true);
const existingHadAuth = initialData && initialData.requires_auth;
if (requiresAuth && !apiKey && !existingHadAuth)
errors.apiKey = intl.formatMessage(i18n.apiKeyRequired);
if (!models) errors.models = intl.formatMessage(i18n.modelsRequired);
@ -577,7 +577,7 @@ export default function CustomProviderForm({
if (step === 'catalog') {
return (
<div className="mt-4">
<ProviderCatalogPicker onSelect={handleTemplateSelect} onCancel={handleCancel} embedded />
<ProviderCatalogPicker onSelect={handleTemplateSelect} />
<div className="flex justify-between pt-4">
<Button type="button" variant="ghost" onClick={handleBackToChoice}>
{intl.formatMessage(i18n.back)}

View file

@ -7,7 +7,6 @@ interface CardContainerProps {
grayedOut: boolean;
testId?: string;
borderStyle?: 'solid' | 'dashed';
className?: string;
}
function GlowingRing() {
@ -33,10 +32,9 @@ export default function CardContainer({
header,
body,
onClick,
grayedOut = false,
grayedOut,
testId,
borderStyle = 'solid',
className = '',
}: CardContainerProps) {
return (
<div
@ -61,8 +59,7 @@ export default function CardContainer({
grayedOut
? 'border-border-primary'
: 'border-border-primary hover:border-border-primary'
}
${className}`}
}`}
>
{header && (
<div style={{ opacity: grayedOut ? '0.5' : '1' }}>

View file

@ -1,16 +1,11 @@
import { useMemo } from 'react';
import CardContainer from './CardContainer';
import CardHeader from './CardHeader';
import CardBody from './CardBody';
import DefaultCardButtons from './buttons/DefaultCardButtons';
import type { ProviderDetails, ProviderMetadata } from '../../../../types/providers';
import type { ProviderDetails } from '../../../../types/providers';
import { defineMessages, useIntl } from '../../../../i18n';
const i18n = defineMessages({
noMetadata: {
id: 'providerCard.noMetadata',
defaultMessage: 'ProviderCard error: No metadata provided',
},
unknownProvider: {
id: 'providerCard.unknownProvider',
defaultMessage: 'Unknown Provider',
@ -35,15 +30,7 @@ export const ProviderCard = function ProviderCard({
isOnboarding,
}: ProviderCardProps) {
const intl = useIntl();
// Safely access metadata with null checks
const providerMetadata: ProviderMetadata | null = provider?.metadata || null;
// Instead of useEffect for logging, use useMemo to memoize the metadata
const metadata = useMemo(() => providerMetadata, [providerMetadata]);
if (!metadata) {
return <div>{intl.formatMessage(i18n.noMetadata)}</div>;
}
const metadata = provider.metadata;
const handleCardClick = () => {
if (!isOnboarding) {
@ -63,9 +50,9 @@ export const ProviderCard = function ProviderCard({
onClick={handleCardClick}
header={
<CardHeader
name={metadata.display_name || provider?.name || intl.formatMessage(i18n.unknownProvider)}
name={metadata.display_name || provider.name || intl.formatMessage(i18n.unknownProvider)}
description={description}
isConfigured={provider?.is_configured || false}
isConfigured={provider.is_configured}
/>
}
body={

View file

@ -80,7 +80,8 @@ const i18n = defineMessages({
},
mlEndpointDescription: {
id: 'securityToggle.mlEndpointDescription',
defaultMessage: 'Enter the full URL for your ML classification service (including model identifier)',
defaultMessage:
'Enter the full URL for your ML classification service (including model identifier)',
},
mlTokenDescription: {
id: 'securityToggle.mlTokenDescription',
@ -110,10 +111,10 @@ interface ClassifierEndpointInputsProps {
disabled: boolean;
endpointPlaceholder: string;
tokenPlaceholder: string;
endpointLabel?: string;
endpointDescription?: string;
tokenLabel?: string;
tokenDescription?: string;
endpointLabel: string;
endpointDescription: string;
tokenLabel: string;
tokenDescription: string;
}
const ClassifierEndpointInputs = ({
@ -185,13 +186,11 @@ export const SecurityToggle = () => {
const { config, upsert } = useConfig();
const promptEnabledOverride = window.appConfig?.get('SECURITY_PROMPT_ENABLED_OVERRIDE') as
| string
| undefined;
string | undefined;
const commandClassifierOverride = window.appConfig?.get(
'SECURITY_COMMAND_CLASSIFIER_ENABLED_OVERRIDE'
) as string | undefined;
const isPromptOverridden =
promptEnabledOverride === 'true' || promptEnabledOverride === 'false';
const isPromptOverridden = promptEnabledOverride === 'true' || promptEnabledOverride === 'false';
const isCommandClassifierOverridden =
commandClassifierOverride === 'true' || commandClassifierOverride === 'false';
const promptOverrideValue = promptEnabledOverride === 'true';

View file

@ -22,14 +22,14 @@ interface ChatProviderProps {
children: ReactNode;
chat: ChatType;
setChat: (chat: ChatType) => void;
contextKey?: string; // Optional context key, defaults to 'hub'
contextKey: string;
}
export const ChatProvider: React.FC<ChatProviderProps> = ({
children,
chat,
setChat,
contextKey = 'hub',
contextKey,
}) => {
const resetChat = () => {
setChat({

View file

@ -18,7 +18,7 @@ interface UseAutoSubmitProps {
messages: Message[];
chatState: ChatState;
initialMessage: UserInput | undefined;
canAutoSubmit?: boolean;
canAutoSubmit: boolean;
handleSubmit: (input: UserInput) => void;
}
@ -32,7 +32,7 @@ export function useAutoSubmit({
messages,
chatState,
initialMessage,
canAutoSubmit = true,
canAutoSubmit,
handleSubmit,
}: UseAutoSubmitProps): UseAutoSubmitReturn {
const [searchParams] = useSearchParams();

View file

@ -16,8 +16,6 @@ export interface NavItem {
path: string;
label: string;
icon: LucideIcon;
getTag?: () => string;
tagAlign?: 'left' | 'right';
}
/** Top-level nav items (excluding Settings which is pinned to the bottom). */

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Sie haben nicht gespeicherte Änderungen"
},
"providerCard.noMetadata": {
"defaultMessage": "ProviderCard-Fehler: Keine Metadaten bereitgestellt"
},
"providerCard.unknownProvider": {
"defaultMessage": "Unbekannter Anbieter"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API-Format"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Abbrechen"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Anbieter auswählen"
},

View file

@ -2747,9 +2747,6 @@
"providerCard.deprecatedReplacement": {
"defaultMessage": "Deprecated — use {replacement} instead."
},
"providerCard.noMetadata": {
"defaultMessage": "ProviderCard error: No metadata provided"
},
"providerCard.unknownProvider": {
"defaultMessage": "Unknown Provider"
},
@ -2759,9 +2756,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API Format"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Cancel"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Choose Provider"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Tienes cambios sin guardar"
},
"providerCard.noMetadata": {
"defaultMessage": "Error de ProviderCard: no se proporcionaron metadatos"
},
"providerCard.unknownProvider": {
"defaultMessage": "Proveedor desconocido"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Formato de API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Cancelar"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Elegir proveedor"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Vous avez des modifications non enregistrées"
},
"providerCard.noMetadata": {
"defaultMessage": "Erreur ProviderCard : aucune métadonnée fournie"
},
"providerCard.unknownProvider": {
"defaultMessage": "Fournisseur inconnu"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Format d'API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Annuler"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Choisir un fournisseur"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "आपके पास सहेजे नहीं गए परिवर्तन हैं"
},
"providerCard.noMetadata": {
"defaultMessage": "प्रदाता कार्ड त्रुटि: कोई मेटाडेटा प्रदान नहीं किया गया"
},
"providerCard.unknownProvider": {
"defaultMessage": "अज्ञात प्रदाता"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API प्रारूप"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "रद्द करें"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "प्रदाता चुनें"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Anda memiliki perubahan yang belum disimpan"
},
"providerCard.noMetadata": {
"defaultMessage": "Kesalahan ProviderCard: Tidak ada metadata yang disediakan"
},
"providerCard.unknownProvider": {
"defaultMessage": "Penyedia Tidak Dikenal"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Format API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Batal"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Pilih Penyedia"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Hai modifiche non salvate"
},
"providerCard.noMetadata": {
"defaultMessage": "Errore ProviderCard: nessun metadato fornito"
},
"providerCard.unknownProvider": {
"defaultMessage": "Provider sconosciuto"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Formato API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Annulla"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Scegli provider"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "未保存の変更があります"
},
"providerCard.noMetadata": {
"defaultMessage": "ProviderCardエラー: メタデータが提供されていません"
},
"providerCard.unknownProvider": {
"defaultMessage": "不明なプロバイダー"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API形式"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "キャンセル"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "プロバイダーを選択"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "저장되지 않은 변경 사항이 있습니다."
},
"providerCard.noMetadata": {
"defaultMessage": "ProviderCard 오류: 메타데이터가 제공되지 않았습니다."
},
"providerCard.unknownProvider": {
"defaultMessage": "알 수 없는 제공업체"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API 형식"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "취소"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "제공업체 선택"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Anda mempunyai perubahan yang belum disimpan"
},
"providerCard.noMetadata": {
"defaultMessage": "Ralat ProviderCard: Tiada metadata disediakan"
},
"providerCard.unknownProvider": {
"defaultMessage": "Penyedia Tidak Diketahui"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Format API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Batal"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Pilih Penyedia"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Tem alterações por guardar"
},
"providerCard.noMetadata": {
"defaultMessage": "Erro de ProviderCard: nenhum metadado fornecido"
},
"providerCard.unknownProvider": {
"defaultMessage": "Fornecedor desconhecido"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Formato da API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Cancelar"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Escolher fornecedor"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "У вас есть несохраненные изменения"
},
"providerCard.noMetadata": {
"defaultMessage": "Ошибка ProviderCard: метаданные не предоставлены"
},
"providerCard.unknownProvider": {
"defaultMessage": "Неизвестный провайдер"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Формат API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Отмена"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Выберите провайдера"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Kaydedilmemiş değişiklikleriniz var"
},
"providerCard.noMetadata": {
"defaultMessage": "ProviderCard hatası: Meta veri sağlanmadı"
},
"providerCard.unknownProvider": {
"defaultMessage": "Bilinmeyen Sağlayıcı"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API Biçimi"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "İptal"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Sağlayıcıyı Seçin"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "Bạn có các thay đổi chưa lưu"
},
"providerCard.noMetadata": {
"defaultMessage": "Lỗi ProviderCard: Không có metadata được cung cấp"
},
"providerCard.unknownProvider": {
"defaultMessage": "Nhà cung cấp không xác định"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "Định dạng API"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "Hủy"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "Chọn nhà cung cấp"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "你有未保存的更改"
},
"providerCard.noMetadata": {
"defaultMessage": "ProviderCard 错误:未提供元数据"
},
"providerCard.unknownProvider": {
"defaultMessage": "未知提供商"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API 格式"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "取消"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "选择提供商"
},

View file

@ -2720,9 +2720,6 @@
"promptsSettings.unsavedChanges": {
"defaultMessage": "您有未儲存的變更"
},
"providerCard.noMetadata": {
"defaultMessage": "ProviderCard 錯誤:未提供中繼資料"
},
"providerCard.unknownProvider": {
"defaultMessage": "未知的提供者"
},
@ -2732,9 +2729,6 @@
"providerCatalogPicker.apiFormat": {
"defaultMessage": "API 格式"
},
"providerCatalogPicker.cancel": {
"defaultMessage": "取消"
},
"providerCatalogPicker.chooseProvider": {
"defaultMessage": "選擇提供者"
},

View file

@ -37,13 +37,13 @@ export const deleteRecipe = async (id: string): Promise<void> => {
await acpDeleteRecipe(id);
};
export const scheduleRecipe = async (id: string, cronSchedule?: string | null): Promise<void> => {
export const scheduleRecipe = async (id: string, cronSchedule: string | null): Promise<void> => {
await acpScheduleRecipe(id, cronSchedule);
};
export const setRecipeSlashCommand = async (
id: string,
slashCommand?: string | null
slashCommand: string | null
): Promise<void> => {
await acpSetRecipeSlashCommand(id, slashCommand);
};

View file

@ -60,7 +60,7 @@ export type UpdateCustomProviderRequest = {
headers?: Record<string, string> | null;
models: string[];
preserves_thinking?: boolean | null;
requires_auth?: boolean;
requires_auth: boolean;
supports_streaming?: boolean | null;
toolshim: boolean;
};

View file

@ -326,11 +326,7 @@ export function registerUpdateIpcHandlers() {
}
// Configure auto-updater
export function setupAutoUpdater(tray?: Tray) {
if (tray) {
trayRef = tray;
}
export function setupAutoUpdater() {
log.info('Setting up auto-updater...');
log.info(`Current app version: ${app.getVersion()}`);
log.info(`Platform: ${process.platform}, Arch: ${process.arch}`);
@ -486,7 +482,11 @@ export function setupAutoUpdater(tray?: Tray) {
if (!autoDownloadDisabled) {
log.info('Auto-downloading update via GitHub fallback on startup...');
await githubAutoDownload(result.downloadUrl!, result.latestVersion!, 'on startup');
await githubAutoDownload(
result.downloadUrl!,
result.latestVersion!,
'on startup'
);
} else {
log.info('Auto-download disabled — skipping GitHub fallback download on startup');
}