Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions web/app/src/hooks/workspace/useAgentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,67 @@ export function useAgentController({
);
}

async function saveAgentPageMetadata(patch: Pick<Partial<AgentDraft>, "description" | "name">): Promise<void> {
const agentID = String(selectedAgentForPage?.id ?? "").trim();
if (!agentID || !agentPageDraft || agentPageBusy) {
return;
}
const payload: AgentUpdatePayload = {};
const isManagerDraft =
isManagerAgent(selectedAgentForPage) ||
agentPageDraft.agent_id === MANAGER_AGENT_ID ||
agentPageDraft.role === MANAGER_AGENT_ROLE;
if (patch.name !== undefined && !isManagerDraft) {
const name = String(patch.name ?? "");
if (!name.trim()) {
setAgentPageError(t("profileSaveIncompleteError"));
return;
}
if (name !== String(agentPageSavedDraft?.name ?? "")) {
payload.name = name;
}
}
if (patch.description !== undefined) {
const description = String(patch.description ?? "");
if (description !== String(agentPageSavedDraft?.description ?? "")) {
payload.description = description;
}
}
if (!Object.keys(payload).length) {
return;
}
setAgentPageBusy(true);
setAgentPageError("");
try {
const saved = await updateAgentRequest(agentID, payload);
setAgentsData((current) => mergeAgentIntoList(current, saved));
setAgentPageDraft((current) => {
if (!current || String(current.agent_id ?? "").trim() !== agentID) {
return current;
}
return {
...current,
...(payload.name !== undefined ? { name: saved.name || "" } : {}),
...(payload.description !== undefined ? { description: saved.description || "" } : {}),
};
});
setAgentPageSavedDraft((current) => {
if (!current || String(current.agent_id ?? "").trim() !== agentID) {
return current;
}
return {
...current,
...(payload.name !== undefined ? { name: saved.name || "" } : {}),
...(payload.description !== undefined ? { description: saved.description || "" } : {}),
};
});
} catch (err) {
setAgentPageError(errorMessage(err, t("agentActionFailed")));
} finally {
setAgentPageBusy(false);
}
}

async function saveAgentPage(): Promise<void> {
const draftToSave = agentPageDraft;
if (!draftToSave || !selectedAgentForPage?.id) {
Expand Down Expand Up @@ -2285,6 +2346,7 @@ export function useAgentController({
workspaceSupported: Boolean(selectedAgentForPage),
onDraftChange: setAgentPageDraft,
onSave: saveAgentPage,
onMetadataSave: saveAgentPageMetadata,
onPublish: publishAgentPage,
onProviderLogin: loginCLIProxyProvider,
onStart: (item: AgentLike | null | undefined) => runAgentAction(item, "start"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { AgentActivityPanel } from "./AgentActivityPanel";

type VoidOrPromise = void | Promise<void>;
type AgentActionHandler = (item: AgentLike) => VoidOrPromise;
type AgentMetadataSavePatch = Pick<Partial<AgentDraft>, "description" | "name">;
type AgentNoticeTone = "info" | "warning" | "success";
const AGENT_PROFILE_TAB_IDS = ["profile", "activity", "channels", "instructions", "skills", "mcp"] as const;
type AgentProfileTabID = (typeof AGENT_PROFILE_TAB_IDS)[number];
Expand Down Expand Up @@ -127,6 +128,7 @@ export type AgentDetailPaneProps = {
onPublish?: () => VoidOrPromise;
onRecreate: AgentActionHandler;
onSave?: () => VoidOrPromise;
onMetadataSave?: (patch: AgentMetadataSavePatch) => VoidOrPromise;
onStart: AgentActionHandler;
onStartFeishuConnect?: AgentActionHandler;
onStop: AgentActionHandler;
Expand Down Expand Up @@ -215,6 +217,7 @@ export function AgentDetailPane({
onStart,
onStop,
onRecreate,
onMetadataSave,
onStartFeishuConnect,
onDisconnectFeishu,
onUpgrade,
Expand All @@ -241,6 +244,7 @@ export function AgentDetailPane({
const [isProfileScrolling, setIsProfileScrolling] = useState(false);
const descriptionInputRef = useRef<HTMLTextAreaElement | null>(null);
const nameInputRef = useRef<HTMLInputElement | null>(null);
const skipMetadataAutosaveRef = useRef(false);
const profileScrollTimerRef = useRef<number | null>(null);
const isManager = isManagerAgent(item);
const canEditAgentName = Boolean(draft && !isManager);
Expand All @@ -258,6 +262,26 @@ export function AgentDetailPane({
hasUnsavedChangesProp ?? Boolean(draft && savedDraft && JSON.stringify(draft) !== JSON.stringify(savedDraft));
const saveDisabled = agentProfilePageSaveDisabled(draft, item, { saving, savedDraft });
const updateDraft = (patch: Partial<AgentDraft>) => onDraftChange?.({ ...(draft || agentToDraft(item)), ...patch });
const saveMetadataPatch = (patch: AgentMetadataSavePatch) => {
if (skipMetadataAutosaveRef.current) {
skipMetadataAutosaveRef.current = false;
return;
}
if (!onMetadataSave || saving) {
return;
}
void onMetadataSave(patch);
};
const cancelNameEdit = () => {
skipMetadataAutosaveRef.current = true;
updateDraft({ name: savedDraft?.name ?? item.name ?? "" });
setIsEditingName(false);
};
const cancelDescriptionEdit = () => {
skipMetadataAutosaveRef.current = true;
updateDraft({ description: savedDraft?.description ?? item.description ?? "" });
setIsEditingDescription(false);
};
const runtimeOptionSchemas = runtimeOptionSchemasForAgent(draft?.runtime_kind || runtimeKind, item);
const fallbackProviderID = String(draft?.model_provider_id || "").trim();
const fallbackModelOptions =
Expand Down Expand Up @@ -450,10 +474,17 @@ export function AgentDetailPane({
value={draft.name}
required
aria-required="true"
onBlur={() => setIsEditingName(false)}
onBlur={(event) => {
setIsEditingName(false);
saveMetadataPatch({ name: event.currentTarget.value });
}}
onInput={(event) => updateDraft({ name: event.currentTarget.value })}
onKeyDown={(event) => {
if (event.key === "Escape" || event.key === "Enter") {
if (event.key === "Escape") {
event.preventDefault();
cancelNameEdit();
event.currentTarget.blur();
} else if (event.key === "Enter") {
event.preventDefault();
event.currentTarget.blur();
}
Expand Down Expand Up @@ -501,11 +532,15 @@ export function AgentDetailPane({
ref={descriptionInputRef}
className="compact-textarea"
value={draft.description}
onBlur={() => setIsEditingDescription(false)}
onBlur={(event) => {
setIsEditingDescription(false);
saveMetadataPatch({ description: event.currentTarget.value });
}}
onInput={(event) => updateDraft({ description: event.currentTarget.value })}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
cancelDescriptionEdit();
event.currentTarget.blur();
}
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Check, CheckCircle2, Edit3, Link2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { AgentAvatarPicker } from "@/components/business/AgentAvatar";
import { Button } from "@/components/ui";
import { localizeRole } from "@/shared/i18n";
import { feishuHumanParticipant } from "@/models/conversations";
import type { IMUser, LocaleCode, TranslateFn } from "@/models/conversations";
Expand Down Expand Up @@ -33,6 +32,7 @@ export function HumanDetailPane({
const [descriptionDraft, setDescriptionDraft] = useState("");
const [isEditingDescription, setIsEditingDescription] = useState(false);
const descriptionInputRef = useRef<HTMLTextAreaElement | null>(null);
const skipDescriptionAutosaveRef = useRef(false);

useEffect(() => {
setDescriptionDraft(String(user?.description || ""));
Expand Down Expand Up @@ -61,6 +61,20 @@ export function HumanDetailPane({
const online = user.is_online !== false;
const currentDescription = String(user.description || "");
const descriptionChanged = descriptionDraft.trim() !== currentDescription.trim();
const saveDescriptionIfChanged = (description: string) => {
if (skipDescriptionAutosaveRef.current) {
skipDescriptionAutosaveRef.current = false;
return;
}
if (!descriptionBusy && description.trim() !== currentDescription.trim()) {
void onDescriptionSave(description);
}
};
const cancelDescriptionEdit = () => {
skipDescriptionAutosaveRef.current = true;
setDescriptionDraft(currentDescription);
setIsEditingDescription(false);
};

return (
<section className="entity-pane human-detail-pane">
Expand Down Expand Up @@ -91,11 +105,15 @@ export function HumanDetailPane({
value={descriptionDraft}
rows={4}
disabled={descriptionBusy}
onBlur={() => setIsEditingDescription(false)}
onBlur={(event) => {
setIsEditingDescription(false);
saveDescriptionIfChanged(event.currentTarget.value);
}}
onChange={(event) => setDescriptionDraft(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
cancelDescriptionEdit();
event.currentTarget.blur();
}
}}
Expand Down Expand Up @@ -141,24 +159,16 @@ export function HumanDetailPane({
) : null}
</div>
<div className="entity-toolbar human-detail-toolbar">
{descriptionChanged || descriptionBusy ? (
<Button
variant="primary"
size="md"
type="button"
loading={descriptionBusy}
loadingLabel={t("agentSavingChanges")}
disabled={!descriptionChanged || descriptionBusy}
onClick={() => void onDescriptionSave(descriptionDraft)}
>
{t("agentSaveChanges")}
</Button>
) : (
{descriptionBusy ? (
<span className="human-save-status" role="status">
{t("agentSavingChanges")}
</span>
) : !descriptionChanged ? (
<span className="human-save-status" role="status">
<Check aria-hidden="true" size={16} strokeWidth={2.5} />
{t("agentSaved")}
</span>
)}
) : null}
</div>
</div>
</header>
Expand Down
Loading