Skip to content
Merged
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
17 changes: 17 additions & 0 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,23 @@
"skillPackagesLabel": "Skill Packages",
"skillPackagesHint": "Mount selected packages when the agent is created. Currently supports only “Full local environment (filesystem + shell)” or “Local filesystem (no shell commands)”, and the storage root must be /.",
"skillPackagesPlaceholder": "Select skill packages",
"manifestWriteFailed": "Page configuration could not be saved (the expert is reloading). It will be retried on the next save.",
"pageConfigTitle": "Page Configuration",
"welcomeMessageTitle": "Title",
"welcomeMessagePlaceholder": "Enter title shown for new chats",
"quickPromptsTitle": "Quick Start Cards",
"addQuickPrompt": "Add Card",
"quickPromptTitle": "Title",
"quickPromptTitlePlaceholder": "Enter card title",
"quickPromptDescription": "Description",
"quickPromptDescriptionPlaceholder": "Enter card description",
"quickPromptContent": "Prompt Content",
"quickPromptContentPlaceholder": "Enter prompt sent when card is clicked",
"quickPromptColor": "Color",
"quickPromptIcon": "Icon",
"quickPromptPreview": "Preview",
"noQuickPrompts": "No quick start cards yet, click the button above to add",
"noIcon": "No Icon",
"iconLabels": {
"sparkles": "Sparkles",
"globe": "Globe",
Expand Down
18 changes: 18 additions & 0 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,24 @@
"skillPackagesLabel": "技能包",
"skillPackagesHint": "创建专家时挂载选中的技能包。目前仅支持「本地完整环境(文件系统+指令执行)」或「本地文件系统(不能执行指令)」,且存储根目录必须为 /。",
"skillPackagesPlaceholder": "选择技能包",
"patchFailed": "保存失败",
"manifestWriteFailed": "页面配置未能保存(专家正在重新加载),将在下次保存时重试。",
"pageConfigTitle": "页面配置",
"welcomeMessageTitle": "标题语",
"welcomeMessagePlaceholder": "输入新建聊天时显示的标题语",
"quickPromptsTitle": "快速启动卡片",
"addQuickPrompt": "添加卡片",
"quickPromptTitle": "标题",
"quickPromptTitlePlaceholder": "输入卡片标题",
"quickPromptDescription": "描述",
"quickPromptDescriptionPlaceholder": "输入卡片描述",
"quickPromptContent": "提示内容",
"quickPromptContentPlaceholder": "输入点击卡片后发送的提示词",
"quickPromptColor": "颜色",
"quickPromptIcon": "图标",
"quickPromptPreview": "预览",
"noQuickPrompts": "暂无快速启动卡片,点击上方按钮添加",
"noIcon": "无图标",
"iconLabels": {
"sparkles": "闪光",
"globe": "地球",
Expand Down
99 changes: 95 additions & 4 deletions dashboard/src/pages/Experts/components/EditAgentDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { request } from "../../../api/request";
import { AgentAdvancedConfigFields } from "../../../components/AgentAdvancedConfigFields";
import ExpertColorPicker from "../../../components/ExpertColorPicker";
import { workspaceApi } from "../../../api/modules/workspace";
import { apiErrorMessage } from "../../../utils/apiError";
import { apiErrorMessage, parseApiError } from "../../../utils/apiError";
import { isAgentChatReady } from "../../../utils/agentError";
import { useAgentFormResources } from "../../../hooks/useAgentFormResources";
import type { OctopAgent } from "../../../context/AgentContext";
Expand All @@ -43,6 +43,7 @@ import {
} from "../../../utils/agentRuntimeConfig";
import { useSkillDisplayName } from "../../Agent/Skills/skillDisplayNames";
import FileEditModal from "./FileEditModal";
import WelcomeConfig, { type WelcomeConfigRef } from "./WelcomeConfig";
import { fetchConfigMdFiles } from "./expertFileGroups";
import {
buildBackendSpec,
Expand Down Expand Up @@ -139,6 +140,44 @@ interface EditAgentDrawerBodyProps {
onSavingChange: (saving: boolean) => void;
}

/**
* Write a workspace file, retrying briefly when the agent's harness is mid-reload.
*
* The PATCH /agents/{aid} endpoint schedules a background ``arebuild_agent``
* that briefly removes the agent from the registry and then re-creates it
* (slow graph compile, often 2-5s on Windows). Workspace writes go through
* ``require_running_workspace`` which raises ``AGENT_NOT_RUNNING`` during
* that absence window. The very first save in a session usually lands
* before the reload starts, but a follow-up save (the user re-opens the
* drawer, edits 页面配置, and clicks save again) hits the reload window.
* Backing off 500ms up to 10 times (~5s) is enough for typical agents; the
* manifest is best-effort so we let the caller's catch surface a warning
* rather than block the save.
*/
async function writeManifestWithRetry(
agentId: string,
path: string,
content: string,
): Promise<void> {
const maxAttempts = 10;
const delayMs = 500;
let lastErr: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
await workspaceApi.createWorkspaceFile(agentId, path, content);
return;
} catch (err) {
lastErr = err;
const code = parseApiError(err)?.code;
if (code !== "AGENT_NOT_RUNNING") throw err;
if (attempt < maxAttempts - 1) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
}
throw lastErr;
}

function EditAgentDrawerBody({
agent,
onClose,
Expand Down Expand Up @@ -173,6 +212,7 @@ function EditAgentDrawerBody({
);
const [listRenameSaving, setListRenameSaving] = useState(false);
const [subagentCatalogOpen, setSubagentCatalogOpen] = useState(false);
const welcomeConfigRef = useRef<WelcomeConfigRef>(null);

const installedSubagentSlugs = useMemo(
() => new Set(agentSubagents.map((s) => s.slug)),
Expand Down Expand Up @@ -308,6 +348,42 @@ function EditAgentDrawerBody({
color: nextColor,
});

// Save welcome config to manifest.json BEFORE patching the agent.
// The PATCH triggers a background harness reload which briefly removes
// the agent from the runtime; writing the workspace file after the
// PATCH can race with that reload and fail with "agent not running".
//
// The PATCH itself never returns AGENT_NOT_RUNNING (it reads the row,
// not the harness entry). The first save usually works because the
// agent is still loaded when we get here. But once a previous save's
// reload is still in flight (the harness arebuild_agent takes a few
// seconds to re-compile the graph), the manifest write below can land
// inside the "agent briefly absent from the registry" window and fail
// with AGENT_NOT_RUNNING — exactly when the user re-opens the drawer,
// expands 页面配置, edits, and saves again. So: retry briefly so the
// in-flight reload can re-register the agent, and fall back to a
// warning (not an error) so the agent's main config still saves.
if (welcomeConfigRef.current && isAgentChatReady(agent.state)) {
const data = welcomeConfigRef.current.getData();
const manifest = {
welcome_message: data.welcome_message,
quick_prompts: data.quick_prompts.filter(
(p) => p.title?.zh || p.title?.en || p.prompt?.zh || p.prompt?.en
),
};
const manifestJson = JSON.stringify(manifest, null, 2);
try {
await writeManifestWithRetry(agent.agent_id, "/manifest.json", manifestJson);
} catch (manifestErr) {
// Manifest is best-effort: the agent's main config (PATCH) is the
// important part. Surface a warning so the user knows, but never
// block the save because of a brief reload race.
message.warning(
apiErrorMessage(manifestErr, t("experts.manifestWriteFailed"), t),
);
}
}

await request(`/agents/${agent.agent_id}`, {
method: "PATCH",
body: JSON.stringify({
Expand All @@ -319,6 +395,7 @@ function EditAgentDrawerBody({
...buildAgentRuntimeRequest(values, { clearMissing: true }),
}),
});

message.success(t("common.save") + " ✓");
if (bwrapToast?.kind === "success") {
message.success(bwrapToast.text);
Expand All @@ -342,6 +419,7 @@ function EditAgentDrawerBody({
}
}, [
agent.agent_id,
agent.state,
agentConfig,
colorPalette,
form,
Expand Down Expand Up @@ -522,7 +600,7 @@ function EditAgentDrawerBody({
</div>
) : (
<>
<div className={styles.drawerSection}>
<div className={styles.drawerSection} style={{ marginBottom: 0 }}>
<div className={styles.drawerSectionTitle}>
{t("experts.basicInfo")}
</div>
Expand Down Expand Up @@ -593,6 +671,7 @@ function EditAgentDrawerBody({
ghost
className={styles.drawerCollapse}
style={{ margin: "8px 0 0", width: "100%" }}
defaultActiveKey={["configFiles"]}
items={[
{
key: "advanced",
Expand All @@ -603,17 +682,29 @@ function EditAgentDrawerBody({
</Form>
),
},
...(isAgentChatReady(agent.state) ? [{
key: "pageConfig",
label: t("experts.pageConfigTitle"),
children: (
<div style={{ padding: 0 }}>
<WelcomeConfig
ref={welcomeConfigRef}
agentId={agent.agent_id}
/>
</div>
),
}] : []),
]}
/>
</div>

{isAgentChatReady(agent.state) && (
<div className={styles.drawerSection}>
<div className={styles.drawerSection} style={{ marginBottom: 0 }}>
<Collapse
ghost
className={styles.drawerCollapse}
defaultActiveKey={["configFiles"]}
style={{ margin: "-4px 0 0", width: "100%" }}
style={{ margin: 0, width: "100%" }}
items={[
{
key: "configFiles",
Expand Down
Loading
Loading