Merge remote-tracking branch 'origin/feat/agent-generation-dry-run-loop' into combined-preview-test

This commit is contained in:
Zamil Majdy
2026-04-02 18:32:05 +02:00
5 changed files with 105 additions and 52 deletions

View File

@@ -6,7 +6,7 @@ import { Text } from "@/components/atoms/Text/Text";
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
import {
@@ -48,12 +48,27 @@ export function SetupRequirementsCard({
const initialValues = useMemo(
() => extractInitialValues(expectedInputs),
[expectedInputs],
// eslint-disable-next-line react-hooks/exhaustive-deps -- stabilise on the raw prop
[output.setup_info.requirements],
);
const [inputValues, setInputValues] =
useState<Record<string, unknown>>(initialValues);
const initialValuesKey = JSON.stringify(initialValues);
useEffect(() => {
setInputValues((prev) => {
const merged = { ...initialValues };
for (const [key, value] of Object.entries(prev)) {
if (value !== undefined && value !== null && value !== "") {
merged[key] = value;
}
}
return merged;
});
// eslint-disable-next-line react-hooks/exhaustive-deps -- sync when serialised values change
}, [initialValuesKey]);
const hasAdvancedFields = expectedInputs.some((i) => i.advanced);
const inputSchema = buildExpectedInputsSchema(expectedInputs, showAdvanced);
@@ -77,7 +92,7 @@ export function SetupRequirementsCard({
needsCredentials &&
[...requiredCredentials].every((key) => !!inputCredentials[key]);
const needsInputs = inputSchema !== null;
const needsInputs = expectedInputs.length > 0;
const requiredInputNames = expectedInputs
.filter((i) => i.required && !i.advanced)
.map((i) => i.name);
@@ -140,26 +155,28 @@ export function SetupRequirementsCard({
</div>
)}
{inputSchema && (
{(inputSchema || hasAdvancedFields) && (
<div className="rounded-2xl border bg-background p-3 pt-4">
<Text variant="small" className="w-fit border-b text-zinc-500">
Inputs
</Text>
<FormRenderer
jsonSchema={inputSchema}
className="mb-3 mt-3"
handleChange={(v) =>
setInputValues((prev) => ({ ...prev, ...(v.formData ?? {}) }))
}
uiSchema={{
"ui:submitButtonOptions": { norender: true },
}}
initialValues={inputValues}
formContext={{
showHandles: false,
size: "small",
}}
/>
{inputSchema && (
<FormRenderer
jsonSchema={inputSchema}
className="mb-3 mt-3"
handleChange={(v) =>
setInputValues((prev) => ({ ...prev, ...(v.formData ?? {}) }))
}
uiSchema={{
"ui:submitButtonOptions": { norender: true },
}}
initialValues={inputValues}
formContext={{
showHandles: false,
size: "small",
}}
/>
)}
{hasAdvancedFields && (
<button
type="button"

View File

@@ -94,6 +94,7 @@ export function CredentialsInput({
handleDeleteCredential,
handleDeleteConfirm,
credentialToDelete,
deleteWarningMessage,
setCredentialToDelete,
isDeletingCredential,
} = hookData;
@@ -202,9 +203,11 @@ export function CredentialsInput({
<DeleteConfirmationModal
credentialToDelete={credentialToDelete}
warningMessage={deleteWarningMessage}
isDeleting={isDeletingCredential}
onClose={() => setCredentialToDelete(null)}
onConfirm={handleDeleteConfirm}
onConfirm={() => handleDeleteConfirm(false)}
onForceConfirm={() => handleDeleteConfirm(true)}
/>
</>
)}

View File

@@ -4,16 +4,20 @@ import { Dialog } from "@/components/molecules/Dialog/Dialog";
interface Props {
credentialToDelete: { id: string; title: string } | null;
warningMessage?: string | null;
isDeleting: boolean;
onClose: () => void;
onConfirm: () => void;
onForceConfirm: () => void;
}
export function DeleteConfirmationModal({
credentialToDelete,
warningMessage,
isDeleting,
onClose,
onConfirm,
onForceConfirm,
}: Props) {
return (
<Dialog
@@ -27,21 +31,35 @@ export function DeleteConfirmationModal({
styling={{ maxWidth: "32rem" }}
>
<Dialog.Content>
<Text variant="large">
Are you sure you want to delete &quot;{credentialToDelete?.title}
&quot;? This action cannot be undone.
</Text>
{warningMessage ? (
<Text variant="large">{warningMessage}</Text>
) : (
<Text variant="large">
Are you sure you want to delete &quot;{credentialToDelete?.title}
&quot;? This action cannot be undone.
</Text>
)}
<Dialog.Footer>
<Button variant="secondary" onClick={onClose} disabled={isDeleting}>
Cancel
</Button>
<Button
variant="destructive"
onClick={onConfirm}
loading={isDeleting}
>
Delete
</Button>
{warningMessage ? (
<Button
variant="destructive"
onClick={onForceConfirm}
loading={isDeleting}
>
Force Delete
</Button>
) : (
<Button
variant="destructive"
onClick={onConfirm}
loading={isDeleting}
>
Delete
</Button>
)}
</Dialog.Footer>
</Dialog.Content>
</Dialog>

View File

@@ -19,6 +19,7 @@ import {
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
import { getHostFromUrl } from "@/lib/utils/url";
import { PlusIcon, TrashIcon } from "@phosphor-icons/react";
import { toast } from "@/components/molecules/Toast/use-toast";
type Props = {
schema: BlockIOCredentialsSubSchema;
@@ -149,22 +150,33 @@ export function HostScopedCredentialsModal({
const existingForHost = allProviderCredentials.filter(
(c) => c.type === "host_scoped" && "host" in c && c.host === host,
);
for (const existing of existingForHost) {
await deleteCredentials(existing.id, true);
try {
for (const existing of existingForHost) {
await deleteCredentials(existing.id, true);
}
const newCredentials = await createHostScopedCredentials({
host,
title: currentHost || host,
headers,
});
onCredentialsCreate({
provider,
id: newCredentials.id,
type: "host_scoped",
title: newCredentials.title,
});
} catch (error) {
const message =
error instanceof Error ? error.message : "Something went wrong";
toast({
title: "Failed to save credentials",
description: message,
variant: "destructive",
});
}
const newCredentials = await createHostScopedCredentials({
host,
title: currentHost || host,
headers,
});
onCredentialsCreate({
provider,
id: newCredentials.id,
type: "host_scoped",
title: newCredentials.title,
});
}
return (

View File

@@ -58,6 +58,9 @@ export function useCredentialsInput({
id: string;
title: string;
} | null>(null);
const [deleteWarningMessage, setDeleteWarningMessage] = useState<
string | null
>(null);
const api = useBackendAPI();
const credentials = useCredentials(schema, siblingInputs);
@@ -297,6 +300,7 @@ export function useCredentialsInput({
}
function handleDeleteCredential(credential: { id: string; title: string }) {
setDeleteWarningMessage(null);
setCredentialToDelete(credential);
}
@@ -319,14 +323,12 @@ export function useCredentialsInput({
if (selectedCredential?.id === credentialToDelete.id) {
onSelectCredential(undefined);
}
setDeleteWarningMessage(null);
setCredentialToDelete(null);
} else if ("need_confirmation" in result && result.need_confirmation) {
toast({
title: "Credential is in use",
description: result.message,
variant: "destructive",
duration: 8000,
});
setDeleteWarningMessage(
result.message || "This credential is in use. Force delete?",
);
}
} catch (error) {
const message =
@@ -364,6 +366,7 @@ export function useCredentialsInput({
isOAuth2FlowInProgress,
cancelOAuthFlow,
credentialToDelete,
deleteWarningMessage,
isDeletingCredential,
actionButtonText: getActionButtonText(
supportsOAuth2,