From 74adcbc0cc1360f3f43cb07771af7650fc897ba1 Mon Sep 17 00:00:00 2001 From: "Angnuo Li (from Dev Box)" Date: Wed, 29 Jul 2026 13:55:52 +0800 Subject: [PATCH 1/3] Add Master Archive specialist agent Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- desktop/renderer/env.d.ts | 3 +- desktop/renderer/src/App.vue | 3 + desktop/renderer/src/components/SidePanel.vue | 4 +- desktop/renderer/src/i18n/en-US.ts | 18 +- desktop/renderer/src/i18n/zh-CN.ts | 15 +- desktop/renderer/src/main.ts | 14 +- desktop/renderer/src/stores/agents.test.ts | 59 +++ desktop/renderer/src/stores/agents.ts | 64 ++- .../renderer/src/views/AgentMarketView.vue | 11 +- desktop/src/agent-personas.test.ts | 211 ++++++++++ desktop/src/agent-personas.ts | 381 ++++++++++++++++++ desktop/src/main.ts | 122 +++--- docs/hero-scenarios.md | 186 +++++++++ 13 files changed, 1006 insertions(+), 85 deletions(-) create mode 100644 desktop/renderer/src/stores/agents.test.ts create mode 100644 desktop/src/agent-personas.test.ts create mode 100644 desktop/src/agent-personas.ts create mode 100644 docs/hero-scenarios.md diff --git a/desktop/renderer/env.d.ts b/desktop/renderer/env.d.ts index cbd7be7..2ea9700 100644 --- a/desktop/renderer/env.d.ts +++ b/desktop/renderer/env.d.ts @@ -23,6 +23,7 @@ interface AppSettings { themeMode: string; accentColor: string; privacyLevel: string; + addedAgentIds: string[]; } type GitHubCopilotLoginEvent = @@ -121,7 +122,7 @@ interface OpenClawAPI { }; settings: { get(): Promise; - set(key: string, value: any): Promise; + set(key: K, value: AppSettings[K]): Promise; }; updates: { check(): Promise; diff --git a/desktop/renderer/src/App.vue b/desktop/renderer/src/App.vue index e5e9d3c..5720403 100644 --- a/desktop/renderer/src/App.vue +++ b/desktop/renderer/src/App.vue @@ -342,6 +342,7 @@ onMounted(async () => { if (s.language) { setLocale(s.language as any); } + agentStore.restoreAddedAgents(s.addedAgentIds); } catch {} // Redirect away from /setup if still on it (e.g. after reload) @@ -469,6 +470,7 @@ onMounted(async () => { } // Fetch scheduled tasks now that gateway is connected taskStore.fetchTasks(); + agentStore.fetchAgents(); // Refresh WeChat login status (important after gateway restart) gateway.refreshWeixinStatus(); }); @@ -503,6 +505,7 @@ onMounted(async () => { sessionStore.reconcileEmptySessions(chatStore.sessionKey, "main"); sessionStore.ensureSession(chatStore.sessionKey, "main"); chatStore.loadHistory(); + agentStore.fetchAgents(); } }); diff --git a/desktop/renderer/src/components/SidePanel.vue b/desktop/renderer/src/components/SidePanel.vue index 3caeaa7..ef5bcdf 100644 --- a/desktop/renderer/src/components/SidePanel.vue +++ b/desktop/renderer/src/components/SidePanel.vue @@ -219,8 +219,8 @@ const flyoutStyle = computed(() => ({ const filteredAgents = computed(() => { const q = agentQuery.value.trim().toLowerCase(); - if (!q) return agentStore.agents; - return agentStore.agents.filter((agent) => { + if (!q) return agentStore.addedAgents; + return agentStore.addedAgents.filter((agent) => { const name = agent.name.toLowerCase(); const desc = (agent.description || "").toLowerCase(); return name.includes(q) || desc.includes(q); diff --git a/desktop/renderer/src/i18n/en-US.ts b/desktop/renderer/src/i18n/en-US.ts index f29267a..c4cc742 100644 --- a/desktop/renderer/src/i18n/en-US.ts +++ b/desktop/renderer/src/i18n/en-US.ts @@ -732,11 +732,27 @@ export default { "agentMarket.pageTitle": "Popular Agents", "agentMarket.add": "Add", "agentMarket.added": "Added", + "agentMarket.persistenceFailed": "Couldn't save the agent selection. Please try again.", // ── Agent: quick tasks expand ── "agent.quickTasks": "Quick Tasks", - // ── Agents (mock data) ── + // ── Agents ── + "agent.masterArchive.name": "Master Archive", + "agent.masterArchive.desc": "Local file organization, batch conversion & document digests", + "agent.masterArchive.tag.1": "File Organization", + "agent.masterArchive.tag.2": "Batch Conversion", + "agent.masterArchive.tag.3": "Document Digests", + "agent.masterArchive.task.1.title": "Convert the Word files in this folder to PDF", + "agent.masterArchive.task.1.desc": + "Batch-convert matching documents while preserving the originals.", + "agent.masterArchive.task.2.title": "Organize this folder - show me the plan first", + "agent.masterArchive.task.2.desc": + "Inventory the files and preview a safe folder structure before moving anything.", + "agent.masterArchive.task.3.title": "Extract deadlines and action items from these documents", + "agent.masterArchive.task.3.desc": + "Consolidate owners, due dates, and source references into a clean checklist.", + "agent.coder.name": "Coder", "agent.coder.desc": "Code development, debugging & code review", "agent.coder.tag.1": "Development", diff --git a/desktop/renderer/src/i18n/zh-CN.ts b/desktop/renderer/src/i18n/zh-CN.ts index 16a6271..942f980 100644 --- a/desktop/renderer/src/i18n/zh-CN.ts +++ b/desktop/renderer/src/i18n/zh-CN.ts @@ -705,11 +705,24 @@ export default { "agentMarket.pageTitle": "热门 Agent", "agentMarket.add": "添加", "agentMarket.added": "已添加", + "agentMarket.persistenceFailed": "无法保存 Agent 选择,请重试。", // ── Agent: quick tasks expand ── "agent.quickTasks": "快捷任务", - // ── Agents (mock data) ── + // ── Agents ── + "agent.masterArchive.name": "归藏大师", + "agent.masterArchive.desc": "本地文件整理、批量转换与文档摘要", + "agent.masterArchive.tag.1": "文件整理", + "agent.masterArchive.tag.2": "批量转换", + "agent.masterArchive.tag.3": "文档摘要", + "agent.masterArchive.task.1.title": "把这个文件夹里的 Word 批量转成 PDF", + "agent.masterArchive.task.1.desc": "保留原文件,并批量转换符合条件的文档。", + "agent.masterArchive.task.2.title": "整理这个文件夹,先给我看方案", + "agent.masterArchive.task.2.desc": "先盘点文件并预览安全的目录结构,确认后再移动。", + "agent.masterArchive.task.3.title": "汇总这些文档中的截止日期和待办", + "agent.masterArchive.task.3.desc": "整理负责人、截止日期和来源,生成清晰的任务清单。", + "agent.coder.name": "程序猿", "agent.coder.desc": "代码开发、调试与代码审查", "agent.coder.tag.1": "代码开发", diff --git a/desktop/renderer/src/main.ts b/desktop/renderer/src/main.ts index 290745a..bb67d2e 100644 --- a/desktop/renderer/src/main.ts +++ b/desktop/renderer/src/main.ts @@ -36,9 +36,21 @@ if (isBrowserDev) { generateSnapshot: noopAsync, }, settings: { - get: () => Promise.resolve(null), + get: () => + Promise.resolve({ + language: "en-US", + autoStart: false, + startMinimized: false, + themeMode: "light", + accentColor: "#1e1f25", + privacyLevel: "balanced", + addedAgentIds: ["main", "coder"], + }), set: noopAsync, }, + agents: { + list: () => Promise.resolve({ agents: [] }), + }, updates: { check: () => Promise.resolve({ diff --git a/desktop/renderer/src/stores/agents.test.ts b/desktop/renderer/src/stores/agents.test.ts new file mode 100644 index 0000000..a18b0a6 --- /dev/null +++ b/desktop/renderer/src/stores/agents.test.ts @@ -0,0 +1,59 @@ +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { setLocale } from "@/i18n"; +import { useAgentStore } from "./agents"; + +describe("agent store", () => { + const settingsSet = vi.fn(); + const agentsList = vi.fn(); + + beforeEach(() => { + setActivePinia(createPinia()); + setLocale("en-US"); + settingsSet.mockReset().mockResolvedValue(undefined); + agentsList.mockReset().mockResolvedValue({ agents: [] }); + window.openclaw = { + settings: { set: settingsSet }, + agents: { list: agentsList }, + } as unknown as typeof window.openclaw; + }); + + it("offers and persists Master Archive as an addable specialist agent", async () => { + const store = useAgentStore(); + const agent = store.marketAgents.find((entry) => entry.id === "master-archive"); + + expect(agent).toMatchObject({ + name: "Master Archive", + isAdded: false, + tags: ["File Organization", "Batch Conversion", "Document Digests"], + }); + expect(agent?.quickTasks).toHaveLength(3); + + await store.toggleAgent("master-archive"); + + expect(store.addedAgents.some((entry) => entry.id === "master-archive")).toBe(true); + expect(settingsSet).toHaveBeenCalledWith( + "addedAgentIds", + expect.arrayContaining(["main", "coder", "master-archive"]), + ); + }); + + it("restores saved selections and clears stale remote agents", async () => { + const store = useAgentStore(); + store.restoreAddedAgents(["main", "master-archive"]); + + expect(store.addedAgents.map((agent) => agent.id)).toEqual(["main", "master-archive"]); + + agentsList.mockResolvedValueOnce({ agents: [{ id: "custom", name: "Custom" }] }); + await store.fetchAgents(); + expect(store.agents.some((agent) => agent.id === "custom")).toBe(true); + + agentsList.mockRejectedValueOnce(new Error("Gateway not connected")); + await store.fetchAgents(); + expect(store.agents.some((agent) => agent.id === "custom")).toBe(true); + + agentsList.mockResolvedValueOnce({ agents: [] }); + await store.fetchAgents(); + expect(store.agents.some((agent) => agent.id === "custom")).toBe(false); + }); +}); diff --git a/desktop/renderer/src/stores/agents.ts b/desktop/renderer/src/stores/agents.ts index c48c660..acf8c76 100644 --- a/desktop/renderer/src/stores/agents.ts +++ b/desktop/renderer/src/stores/agents.ts @@ -74,6 +74,33 @@ const AGENT_DEFS: AgentDef[] = [ ], isAdded: true, }, + { + id: "master-archive", + nameKey: "agent.masterArchive.name", + descKey: "agent.masterArchive.desc", + avatar: getAvatarUrl("Archaeologist.png"), + image: getAvatarUrl("Archaeologist.png"), + tagKeys: [ + "agent.masterArchive.tag.1", + "agent.masterArchive.tag.2", + "agent.masterArchive.tag.3", + ], + taskKeys: [ + { + titleKey: "agent.masterArchive.task.1.title", + descKey: "agent.masterArchive.task.1.desc", + }, + { + titleKey: "agent.masterArchive.task.2.title", + descKey: "agent.masterArchive.task.2.desc", + }, + { + titleKey: "agent.masterArchive.task.3.title", + descKey: "agent.masterArchive.task.3.desc", + }, + ], + isAdded: false, + }, { id: "coder", nameKey: "agent.coder.name", @@ -160,6 +187,8 @@ const AGENT_DEFS: AgentDef[] = [ }, ]; +const DEFAULT_ADDED_AGENT_IDS = AGENT_DEFS.filter((def) => def.isAdded).map((def) => def.id); + export const useAgentStore = defineStore("agents", () => { const agentIsAdded = ref>( Object.fromEntries(AGENT_DEFS.map((d) => [d.id, d.isAdded])), @@ -179,7 +208,9 @@ export const useAgentStore = defineStore("agents", () => { // metadata (avatars, tags, quick tasks). Keep the local persona for any // id we know about and only append genuinely custom remote agents. const localIds = new Set(localAgents.map((a) => a.id)); - const extraRemote = remoteAgents.value.filter((a) => !localIds.has(a.id)); + const extraRemote = remoteAgents.value + .filter((a) => !localIds.has(a.id)) + .map((agent) => ({ ...agent, isAdded: true })); return [...localAgents, ...extraRemote]; }); const currentAgentId = ref("main"); @@ -188,9 +219,25 @@ export const useAgentStore = defineStore("agents", () => { const marketAgents = computed(() => agents.value.filter((a) => a.id !== "main")); - function toggleAgent(agentId: string) { - if (agentId in agentIsAdded.value) { - agentIsAdded.value[agentId] = !agentIsAdded.value[agentId]; + function restoreAddedAgents(agentIds?: string[]) { + const restoredIds = new Set(Array.isArray(agentIds) ? agentIds : DEFAULT_ADDED_AGENT_IDS); + agentIsAdded.value = Object.fromEntries( + AGENT_DEFS.map((def) => [def.id, def.id === "main" || restoredIds.has(def.id)]), + ); + } + + async function toggleAgent(agentId: string) { + if (!(agentId in agentIsAdded.value) || agentId === "main") return; + const previous = agentIsAdded.value[agentId]; + agentIsAdded.value[agentId] = !previous; + const addedAgentIds = AGENT_DEFS.filter((def) => agentIsAdded.value[def.id]).map( + (def) => def.id, + ); + try { + await window.openclaw.settings.set("addedAgentIds", addedAgentIds); + } catch (error) { + agentIsAdded.value[agentId] = previous; + throw error; } } @@ -201,11 +248,9 @@ export const useAgentStore = defineStore("agents", () => { async function fetchAgents() { try { const result = await window.openclaw.agents.list(); - if (result.agents && result.agents.length > 0) { - remoteAgents.value = result.agents as Agent[]; - } - } catch { - // Gateway may not support agents.list yet — keep defaults + remoteAgents.value = Array.isArray(result.agents) ? (result.agents as Agent[]) : []; + } catch (error) { + console.warn("[agents] Failed to refresh gateway agents:", error); } } @@ -214,6 +259,7 @@ export const useAgentStore = defineStore("agents", () => { currentAgentId, addedAgents, marketAgents, + restoreAddedAgents, toggleAgent, selectAgent, fetchAgents, diff --git a/desktop/renderer/src/views/AgentMarketView.vue b/desktop/renderer/src/views/AgentMarketView.vue index e936f11..ae77424 100644 --- a/desktop/renderer/src/views/AgentMarketView.vue +++ b/desktop/renderer/src/views/AgentMarketView.vue @@ -9,7 +9,7 @@ v-for="agent in filteredAgents" :key="agent.id" :agent="agent" - @toggle="agentStore.toggleAgent" + @toggle="handleToggle" /> @@ -17,6 +17,7 @@