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
5 changes: 4 additions & 1 deletion desktop/renderer/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ interface OpenClawAPI {
};
settings: {
get(): Promise<AppSettings>;
set(key: string, value: any): Promise<void>;
set<K extends keyof AppSettings>(key: K, value: AppSettings[K]): Promise<void>;
};
updates: {
check(): Promise<UpdateCheckResult>;
Expand Down Expand Up @@ -167,6 +167,9 @@ interface OpenClawAPI {
};
agents: {
list(): Promise<{ agents?: { id: string; name: string; description?: string }[] }>;
add(agentId: string): Promise<{
agents: { id: string; name: string; description?: string }[];
}>;
};
channels: {
list(): Promise<{
Expand Down
2 changes: 2 additions & 0 deletions desktop/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,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();
});
Expand Down Expand Up @@ -503,6 +504,7 @@ onMounted(async () => {
sessionStore.reconcileEmptySessions(chatStore.sessionKey, "main");
sessionStore.ensureSession(chatStore.sessionKey, "main");
chatStore.loadHistory();
agentStore.fetchAgents();
}
});

Expand Down
9 changes: 3 additions & 6 deletions desktop/renderer/src/components/agent/AgentCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@
<div v-if="agent.tags?.length" class="agent-card-tags">
<span v-for="tag in agent.tags" :key="tag" class="agent-card-tag">{{ tag }}</span>
</div>
<span v-if="agent.isAdded" class="agent-card-added-text" @click="$emit('toggle', agent.id)">{{
t("agentMarket.added")
}}</span>
<button v-else class="agent-card-action" @click="$emit('toggle', agent.id)">
<span v-if="agent.isAdded" class="agent-card-added-text">{{ t("agentMarket.added") }}</span>
<button v-else class="agent-card-action" @click="$emit('add', agent.id)">
<IconPlus :size="14" />
{{ t("agentMarket.add") }}
</button>
Expand All @@ -35,7 +33,7 @@ defineProps<{
}>();

defineEmits<{
toggle: [agentId: string];
add: [agentId: string];
}>();
</script>

Expand Down Expand Up @@ -85,7 +83,6 @@ defineEmits<{
font-size: 12px;
font-weight: 600;
color: var(--success);
cursor: pointer;
}

.agent-card-avatar {
Expand Down
18 changes: 17 additions & 1 deletion desktop/renderer/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,11 +732,27 @@ export default {
"agentMarket.pageTitle": "Popular Agents",
"agentMarket.add": "Add",
"agentMarket.added": "Added",
"agentMarket.addFailed": "Couldn't add the agent to OpenClaw. 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",
Expand Down
15 changes: 14 additions & 1 deletion desktop/renderer/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,11 +705,24 @@ export default {
"agentMarket.pageTitle": "热门 Agent",
"agentMarket.add": "添加",
"agentMarket.added": "已添加",
"agentMarket.addFailed": "无法将 Agent 添加到 OpenClaw,请重试。",

// ── 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": "代码开发",
Expand Down
20 changes: 19 additions & 1 deletion desktop/renderer/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,27 @@ 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",
}),
set: noopAsync,
},
agents: {
list: () => Promise.resolve({ agents: [] }),
add: (agentId: string) =>
Promise.resolve({
agents: [
{ id: "main", name: "Assistant" },
{ id: agentId, name: agentId },
],
}),
},
updates: {
check: () =>
Promise.resolve({
Expand Down
92 changes: 92 additions & 0 deletions desktop/renderer/src/stores/agents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 agentsList = vi.fn();
const agentsAdd = vi.fn();

beforeEach(() => {
setActivePinia(createPinia());
setLocale("en-US");
agentsList
.mockReset()
.mockResolvedValue({ agents: [{ id: "main", name: "Assistant" }] });
agentsAdd.mockReset();
window.openclaw = {
agents: { list: agentsList, add: agentsAdd },
} as unknown as typeof window.openclaw;
});

it("shows only the OpenClaw roster in the sidebar", async () => {
const store = useAgentStore();

expect(store.agents.map((agent) => agent.id)).toEqual(["main"]);

agentsList.mockResolvedValueOnce({
agents: [
{ id: "main", name: "Assistant" },
{ id: "custom", name: "Custom Agent" },
],
});
await store.fetchAgents();
expect(store.agents.map((agent) => agent.id)).toEqual(["main", "custom"]);
});

it("adds Master Archive to OpenClaw before showing it in the sidebar", async () => {
const store = useAgentStore();
const marketAgent = store.marketAgents.find(
(entry) => entry.id === "master-archive",
);

expect(marketAgent).toMatchObject({
name: "Master Archive",
isAdded: false,
tags: ["File Organization", "Batch Conversion", "Document Digests"],
});
expect(marketAgent?.quickTasks).toHaveLength(3);

agentsAdd.mockResolvedValueOnce({
agents: [
{ id: "main", name: "Assistant" },
{ id: "master-archive", name: "Master Archive" },
],
});
await store.addAgent("master-archive");

expect(agentsAdd).toHaveBeenCalledWith("master-archive");
expect(store.agents.map((agent) => agent.id)).toEqual([
"main",
"master-archive",
]);
expect(
store.marketAgents.find((agent) => agent.id === "master-archive")?.isAdded,
).toBe(true);
});

it("keeps the roster unchanged when Add fails", async () => {
const store = useAgentStore();
agentsAdd.mockRejectedValueOnce(new Error("Gateway restart failed"));

await expect(store.addAgent("master-archive")).rejects.toThrow(
"Gateway restart failed",
);
expect(store.agents.map((agent) => agent.id)).toEqual(["main"]);
expect(
store.marketAgents.find((agent) => agent.id === "master-archive")?.isAdded,
).toBe(false);
});

it("preserves the last real roster when refresh fails", async () => {
const store = useAgentStore();
agentsList.mockResolvedValueOnce({
agents: [{ id: "custom", name: "Custom Agent" }],
});
await store.fetchAgents();
agentsList.mockRejectedValueOnce(new Error("Gateway not connected"));
await store.fetchAgents();

expect(store.agents.map((agent) => agent.id)).toEqual(["custom"]);
});
});
Loading
Loading