From 6e5ba189ff546c9d6f6680791b3506bbc0317f4f Mon Sep 17 00:00:00 2001 From: wanghj Date: Thu, 11 Jun 2026 18:01:25 +0800 Subject: [PATCH 1/2] fix(web): restore manager profile save after avatar auto-save Stop avatar edits from submitting incomplete LLM profiles, validate required model and API fields before save, and sync manager runtime in the background with clearer incomplete-setup feedback. --- .../src/hooks/workspace/useAgentController.ts | 39 +++++++++++------ web/app/src/models/agents.ts | 42 ++++++++++++++++--- .../AgentDetailPane/AgentDetailPane.css | 10 +++++ .../AgentDetailPane/AgentDetailPane.tsx | 31 ++++++-------- web/app/src/shared/i18n/messages.ts | 6 +++ web/app/tests/models/agents.test.ts | 31 +++++++++++++- 6 files changed, 121 insertions(+), 38 deletions(-) diff --git a/web/app/src/hooks/workspace/useAgentController.ts b/web/app/src/hooks/workspace/useAgentController.ts index 4b7e76c6..f2419b36 100644 --- a/web/app/src/hooks/workspace/useAgentController.ts +++ b/web/app/src/hooks/workspace/useAgentController.ts @@ -36,8 +36,11 @@ import { applyTemplateToDraft, advanceAgentProgress, agentDraftWithRuntimeFieldsFromAgent, + agentProfilePageSaveDisabled, agentRuntimePollSettled, agentToDraft, + isAgentProfileDraftComplete, + isAgentProfileMarkedComplete, availableManagerRebuildRuntimeOptions, collectManagerTemplateVariants, defaultManagerRebuildImageForRuntime, @@ -259,6 +262,7 @@ export function useAgentController({ const { models: agentPageModels, modelBusy: agentPageModelBusy, + modelError: agentPageModelError, resetModels: resetAgentPageModels, } = useProfileModelOptions({ draft: agentPageDraft, @@ -830,11 +834,15 @@ export function useAgentController({ }); } - async function saveAgentPage(draftOverride?: AgentDraft): Promise { - const draftToSave = draftOverride ?? agentPageDraft; + async function saveAgentPage(): Promise { + const draftToSave = agentPageDraft; if (!draftToSave || !selectedAgentForPage?.id) { return; } + if (agentProfilePageSaveDisabled(draftToSave, selectedAgentForPage)) { + setAgentPageError(t("profileSaveIncompleteError")); + return; + } setAgentPageBusy(true); setAgentPageError(""); try { @@ -895,12 +903,17 @@ export function useAgentController({ setAgentPageSavedDraft(nextDraft); return; } + if (profileChanged && !isAgentProfileDraftComplete(draftToSave)) { + setAgentPageError(t("profileSaveIncompleteError")); + return; + } debugAgentPageSavePayload("full", payload); const managerBeforeSave = selectedAgentForPage; + const profileIncompleteBeforeSave = !isAgentProfileMarkedComplete(agentPageSavedDraft); const saved = await updateAgentRequest(selectedAgentForPage.id, payload); await refreshAgentsWithUpdatedAgent(saved); if (saved.id === MANAGER_AGENT_ID && profileChanged) { - await syncManagerRuntimeAfterProfileSave(managerBeforeSave); + void syncManagerRuntimeAfterProfileSave(managerBeforeSave, profileIncompleteBeforeSave); } await refreshWorkspaceBootstrap(); if (saved.id === MANAGER_AGENT_ID) { @@ -910,6 +923,15 @@ export function useAgentController({ const savedDraft = await agentDraftFromItem(saved); setAgentPageDraft(savedDraft); setAgentPageSavedDraft(savedDraft); + if ( + profileChanged && + saved.id === MANAGER_AGENT_ID && + !isAgentProfileMarkedComplete(saved) && + !isAgentProfileMarkedComplete(savedDraft) + ) { + setAgentPageError(t("profileSaveIncompleteError")); + showAgentPageNotice(t("profileSetupIncompleteAfterSave")); + } } catch (err) { setAgentPageError(errorMessage(err, t("agentActionFailed"))); } finally { @@ -917,15 +939,6 @@ export function useAgentController({ } } - async function saveAgentPageAvatar(avatar: string): Promise { - if (!agentPageDraft) { - return; - } - const nextDraft = { ...agentPageDraft, avatar }; - setAgentPageDraft(nextDraft); - await saveAgentPage(nextDraft); - } - async function publishAgentPage(): Promise { if (!selectedAgentForPage?.id || agentPagePublishBusy) { return; @@ -1298,6 +1311,7 @@ export function useAgentController({ hasUnsavedChanges: agentPageHasUnsavedChanges, models: agentPageModels, modelBusy: agentPageModelBusy, + modelError: agentPageModelError, saving: agentPageBusy, publishBusy: agentPagePublishBusy, saveError: agentPageError, @@ -1316,7 +1330,6 @@ export function useAgentController({ onSelectWorkspaceFile: setSelectedAgentWorkspacePath, onDraftChange: setAgentPageDraft, onSave: saveAgentPage, - onAvatarSave: saveAgentPageAvatar, onPublish: publishAgentPage, onProviderLogin: loginCLIProxyProvider, onStart: (item: AgentLike | null | undefined) => runAgentAction(item, "start"), diff --git a/web/app/src/models/agents.ts b/web/app/src/models/agents.ts index 4198f276..44eb0a0b 100644 --- a/web/app/src/models/agents.ts +++ b/web/app/src/models/agents.ts @@ -1057,12 +1057,34 @@ export function isAgentProfileDraftComplete(draft: Partial | null | if (!String(draft?.model_id ?? "").trim()) { return false; } - if (draft?.provider === "api" && !String(draft.base_url ?? "").trim()) { - return false; + if (draft?.provider === "api") { + if (!String(draft.base_url ?? "").trim()) { + return false; + } + if (!String(draft.api_key ?? "").trim() && !draft.api_key_set) { + return false; + } } return true; } +export function agentProfilePageSaveDisabled( + draft: AgentDraft | null | undefined, + item: AgentLike | null | undefined, + options: { saving?: boolean } = {}, +): boolean { + if (options.saving || !draft) { + return true; + } + if (!String(draft.name ?? "").trim()) { + return true; + } + if (isNotifierRuntimeDraftOnAgentPage(draft, item)) { + return !notifierFormIsComplete(draft, item); + } + return !isAgentProfileDraftComplete(draft); +} + export function isAgentIncomplete( item: AgentLike | null | undefined, draftOverride?: AgentDraft | null | undefined, @@ -1110,11 +1132,19 @@ export function agentRuntimePollSettled(item: AgentLike | null | undefined): boo if (isAgentRunning(item)) { return true; } - if (!String(item?.box_id ?? "").trim()) { - return false; - } const status = String(item?.status ?? "").toLowerCase(); - return status === "stopped" || status === "offline" || status === "failed" || status === "error"; + if (status === "stopped" || status === "offline" || status === "failed" || status === "error") { + return true; + } + if ( + isAgentProfileMarkedComplete(item) && + status !== "profile_incomplete" && + status !== "starting" && + status !== "provisioning" + ) { + return true; + } + return false; } export function isAgentUpgradeNeeded(item: AgentLike | null | undefined): boolean { diff --git a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css index 1139fe6c..4aa471c3 100644 --- a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css +++ b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.css @@ -81,6 +81,16 @@ white-space: nowrap; } +.agent-save-status.warn { + color: oklch(0.72 0.16 35); +} + +.field-hint.error { + display: block; + margin-top: 6px; + color: oklch(0.72 0.16 35); +} + .agent-actions-menu-trigger { position: relative; } diff --git a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx index 405ae2fe..19cf87ad 100644 --- a/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx +++ b/web/app/src/pages/AgentPage/components/AgentDetailPane/AgentDetailPane.tsx @@ -1,16 +1,16 @@ import { Check, MoreHorizontal } from "lucide-react"; +import { errorMessage } from "@/api/client"; import { PROVIDERS, REASONING_EFFORTS, SHOW_AGENT_LIFECYCLE_ACTIONS } from "@/shared/constants/agents"; import { APIKeyField, CLIProxyAuthControl, EnvKeyValueEditor, - isBlank, NotifierControls, - profileBaseURLMissing, requiredFieldLabel, } from "@/components/business/ProfileControls"; import { WorkspaceFilePreview, WorkspaceFileTree } from "@/components/business/WorkspaceFileTree"; import { + agentProfilePageSaveDisabled, agentStatusLabel, agentModelID, agentToDraft, @@ -23,7 +23,6 @@ import { isNotifierRuntimeDraftOnAgentPage, normalizeAuthProviderName, normalizeRuntimeKind, - notifierFormIsComplete, } from "@/models/agents"; import type { AgentDraft, AgentLike } from "@/models/agents"; import type { IMConversation, TranslateFn } from "@/models/conversations"; @@ -54,12 +53,12 @@ export type AgentDetailPaneProps = { hasUnsavedChanges?: boolean; item: AgentLike; modelBusy?: boolean; + modelError?: unknown; models?: string[]; notice?: string; notifierWebhookPublicOrigin?: string; onDelete: AgentActionHandler; onDraftChange?: (draft: AgentDraft) => void; - onAvatarSave?: (avatar: string) => VoidOrPromise; onInvite: AgentActionHandler; onOpenDM: AgentActionHandler; onProviderLogin?: (provider: string) => VoidOrPromise; @@ -97,6 +96,7 @@ export function AgentDetailPane({ models = [], notice = "", modelBusy = false, + modelError = null, saving = false, publishBusy = false, saveError = "", @@ -113,7 +113,6 @@ export function AgentDetailPane({ workspaceFileError = "", onSelectWorkspaceFile = () => {}, onDraftChange, - onAvatarSave, onSave, onPublish, onProviderLogin, @@ -137,12 +136,7 @@ export function AgentDetailPane({ const canPublish = runtimeKind === "picoclaw_sandbox" || runtimeKind === "openclaw_sandbox"; const hasUnsavedChanges = hasUnsavedChangesProp ?? Boolean(draft && savedDraft && JSON.stringify(draft) !== JSON.stringify(savedDraft)); - const saveDisabled = - saving || - isBlank(draft?.name) || - (isNotifierRuntimeDraftOnAgentPage(draft, item) - ? !notifierFormIsComplete(draft, item) - : !draft?.model_id || profileBaseURLMissing(draft)); + const saveDisabled = agentProfilePageSaveDisabled(draft, item, { saving }); const updateDraft = (patch: Partial) => onDraftChange?.({ ...(draft || agentToDraft(item)), ...patch }); return (
@@ -153,13 +147,7 @@ export function AgentDetailPane({ value={draft.avatar || item.avatar} t={t} mode="edit" - onChange={(avatar) => { - if (onAvatarSave) { - void onAvatarSave(avatar); - return; - } - updateDraft({ avatar }); - }} + onChange={(avatar) => updateDraft({ avatar })} /> ) : ( @@ -234,6 +222,10 @@ export function AgentDetailPane({ > {t("agentSaveChanges")} + ) : draft && incomplete ? ( + + {t("agentProfileSetupRequired")} + ) : draft ? (