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
2 changes: 1 addition & 1 deletion desktop/renderer/src/browser-openclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function createBrowserOpenClawMock(): OpenClawAPI {
const browserAgentRoster = [
{ id: "main", name: "Assistant" },
{ id: "master-archive", name: "Master Archive" },
{ id: "painter", name: "Van Gogh" },
{ id: "office-artisan", name: "Office Artisan" },
{ id: "dr-pulse", name: "Dr. Pulse" },
{ id: "intel-analyst", name: "Intel Analyst" },
{ id: "market-sentinel", name: "Market Sentinel" },
Expand Down
26 changes: 15 additions & 11 deletions desktop/renderer/src/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,17 +782,21 @@ export default {
"agent.codeGeek.task.3.desc":
"Trace the first relevant error to its root cause, apply a fix, and verify the command.",

"agent.painter.name": "Van Gogh",
"agent.painter.desc": "Pixel-style illustration & scene creation",
"agent.painter.tag.1": "Illustration",
"agent.painter.tag.2": "Style Analysis",
"agent.painter.tag.3": "Scene Design",
"agent.painter.task.1.title": "Generate a cyberpunk cityscape",
"agent.painter.task.1.desc": "Create a futuristic cityscape illustration with neon lights.",
"agent.painter.task.2.title": "Design a game character portrait",
"agent.painter.task.2.desc": "Draw a stylized character concept based on your description.",
"agent.painter.task.3.title": "Create pixel emoji stickers",
"agent.painter.task.3.desc": "Generate cute retro pixel-style emoji stickers.",
"agent.officeArtisan.name": "Office Artisan",
"agent.officeArtisan.desc":
"Validated Word reports, Excel analysis & PowerPoint presentations from source material",
"agent.officeArtisan.tag.1": "Word Reports",
"agent.officeArtisan.tag.2": "Excel Analysis",
"agent.officeArtisan.tag.3": "Presentation Decks",
"agent.officeArtisan.task.1.title": "Turn these materials into a formal Word report",
"agent.officeArtisan.task.1.desc":
"Create a structured, sourced report in a new file and validate its layout and contents.",
"agent.officeArtisan.task.2.title": "Analyze this workbook and create a chart summary",
"agent.officeArtisan.task.2.desc":
"Preserve raw data, add reproducible calculations and charts, and verify formulas and ranges.",
"agent.officeArtisan.task.3.title": "Turn this report into a presentation deck",
"agent.officeArtisan.task.3.desc":
"Build a concise, source-faithful deck and validate slide order, readability, and overflow.",

"agent.drPulse.name": "Dr. Pulse",
"agent.drPulse.desc": "Evidence-led Windows diagnostics, guided tuning & scenario preparation",
Expand Down
8 changes: 8 additions & 0 deletions desktop/renderer/src/i18n/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ describe("i18n", () => {
setLocale("en-US"); // restore
});

it("Office Artisan catalog keys exist in both locales", () => {
setLocale("en-US");
expect(t("agent.officeArtisan.name")).toBe("Office Artisan");
setLocale("zh-CN");
expect(t("agent.officeArtisan.name")).toBe("办公巧匠");
setLocale("en-US"); // restore
});

it("Creative Muse catalog keys exist in both locales", () => {
setLocale("en-US");
expect(t("agent.creativeMuse.name")).toBe("Creative Muse");
Expand Down
22 changes: 11 additions & 11 deletions desktop/renderer/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,17 +743,17 @@ export default {
"agent.codeGeek.task.3.title": "诊断这个构建失败",
"agent.codeGeek.task.3.desc": "定位首个关键错误的根因,完成修复并重新验证命令。",

"agent.painter.name": "梵高",
"agent.painter.desc": "像素风格插画与场景创作",
"agent.painter.tag.1": "插画创作",
"agent.painter.tag.2": "风格分析",
"agent.painter.tag.3": "场景设计",
"agent.painter.task.1.title": "生成赛博朋克城市图",
"agent.painter.task.1.desc": "创作霓虹灯下的未来都市场景插画。",
"agent.painter.task.2.title": "设计游戏角色立绘",
"agent.painter.task.2.desc": "根据描述绘制风格化的角色概念图。",
"agent.painter.task.3.title": "制作像素表情包",
"agent.painter.task.3.desc": "生成可爱复古的像素风格表情贴纸。",
"agent.officeArtisan.name": "办公巧匠",
"agent.officeArtisan.desc": "根据源材料生成并验证 Word 报告、Excel 分析和 PowerPoint 演示",
"agent.officeArtisan.tag.1": "Word 报告",
"agent.officeArtisan.tag.2": "Excel 分析",
"agent.officeArtisan.tag.3": "演示文稿",
"agent.officeArtisan.task.1.title": "把这些材料整理成正式 Word 报告",
"agent.officeArtisan.task.1.desc": "在新文件中生成结构化、有来源的报告,并验证版式与内容。",
"agent.officeArtisan.task.2.title": "分析这份 Excel 并生成图表摘要",
"agent.officeArtisan.task.2.desc": "保留原始数据,添加可复算的公式和图表,并检查公式与数据范围。",
"agent.officeArtisan.task.3.title": "把这份报告做成 PowerPoint 演示",
"agent.officeArtisan.task.3.desc": "生成忠于来源的精炼演示,并检查顺序、可读性和内容溢出。",

"agent.drPulse.name": "系统神医",
"agent.drPulse.desc": "循证 Windows 诊断、引导式调优与场景准备",
Expand Down
21 changes: 21 additions & 0 deletions desktop/renderer/src/stores/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ describe("agent store", () => {
agentsList.mockResolvedValueOnce({
agents: [
{ id: "main", name: "Assistant" },
{ id: "office-artisan", name: "Office Artisan" },
{ id: "painter", name: "Office Artisan" },
{ id: "dr-pulse", name: "Dr. Pulse" },
{ id: "master", name: "Dr. Pulse" },
{ id: "market-sentinel", name: "Market Sentinel" },
Expand All @@ -56,6 +58,7 @@ describe("agent store", () => {

expect(store.agents.map((agent) => agent.id)).toEqual([
"main",
"office-artisan",
"dr-pulse",
"market-sentinel",
"creative-muse",
Expand Down Expand Up @@ -97,6 +100,24 @@ describe("agent store", () => {
});
});

it("presents the Office Artisan validated Office scenarios", () => {
const store = useAgentStore();

expect(store.marketAgents.find((entry) => entry.id === "office-artisan")).toMatchObject({
name: "Office Artisan",
description:
"Validated Word reports, Excel analysis & PowerPoint presentations from source material",
tags: ["Word Reports", "Excel Analysis", "Presentation Decks"],
isAdded: false,
});
expect(store.marketAgents.find((entry) => entry.id === "office-artisan")?.quickTasks).toEqual([
expect.objectContaining({ title: "Turn these materials into a formal Word report" }),
expect.objectContaining({ title: "Analyze this workbook and create a chart summary" }),
expect.objectContaining({ title: "Turn this report into a presentation deck" }),
]);
expect(store.marketAgents.some((entry) => entry.id === "painter")).toBe(false);
});

it("presents the Intel Analyst research scenarios", () => {
const store = useAgentStore();

Expand Down
7 changes: 7 additions & 0 deletions desktop/renderer/src/stores/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,13 @@ describe("useChatStore — draft sessions", () => {
expect(store.sessionKey).toMatch(/^agent:leopard:session-/);
expect(store.currentSessionAgentId).toBe("market-sentinel");
});

it("attributes historical Painter session keys to Office Artisan", () => {
const store = useChatStore();
store.newSession("painter");
expect(store.sessionKey).toMatch(/^agent:painter:session-/);
expect(store.currentSessionAgentId).toBe("office-artisan");
});
});

describe("useChatStore — attachments", () => {
Expand Down
22 changes: 22 additions & 0 deletions desktop/renderer/src/stores/sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,28 @@ describe("useSessionStore", () => {
});
});

it("keeps historical Painter sessions visible under Office Artisan", () => {
storage["openclaw-sessions"] = JSON.stringify([
{
key: "agent:painter:legacy",
title: "Legacy design session",
createdAt: 1,
updatedAt: 1,
preview: "slides",
agentId: "painter",
},
]);
setActivePinia(createPinia());

const store = useSessionStore();

expect(store.sessions[0]).toMatchObject({
key: "agent:painter:legacy",
agentId: "office-artisan",
title: "Legacy design session",
});
});

it("removes a persisted reserved warm-up session on load", () => {
storage["openclaw-sessions"] = JSON.stringify([
{
Expand Down
12 changes: 12 additions & 0 deletions desktop/src/agent-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,18 @@ describe("agent catalog skills binding", () => {
expect(canonicalAgentId("leopard")).toBe("market-sentinel");
});

it("replaces Painter with the dedicated Office Artisan persona", () => {
expect(AGENT_CATALOG.some((agent) => agent.id === "painter")).toBe(false);
expect(AGENT_CATALOG.find((agent) => agent.id === "office-artisan")).toMatchObject({
name: "Office Artisan",
avatar: "office-artisan-avatar.png",
image: "Painter.png",
workspaceDirName: "workspace-office-artisan",
personaProfile: "office-artisan",
});
expect(canonicalAgentId("painter")).toBe("office-artisan");
});

it("replaces Singer with the dedicated Creative Muse persona", () => {
expect(AGENT_CATALOG.some((agent) => agent.id === "singer")).toBe(false);
expect(AGENT_CATALOG.find((agent) => agent.id === "creative-muse")).toMatchObject({
Expand Down
35 changes: 26 additions & 9 deletions desktop/src/agent-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface AgentCatalogEntry {
| "dr-pulse"
| "intel-analyst"
| "market-sentinel"
| "office-artisan"
| "creative-muse";
}

Expand Down Expand Up @@ -132,20 +133,35 @@ export const AGENT_CATALOG: readonly AgentCatalogEntry[] = [
personaProfile: "code-geek",
},
{
id: "painter",
name: "Painter",
nameKey: "agent.painter.name",
descKey: "agent.painter.desc",
avatar: "painter-avatar.png",
id: "office-artisan",
name: "Office Artisan",
nameKey: "agent.officeArtisan.name",
descKey: "agent.officeArtisan.desc",
avatar: "office-artisan-avatar.png",
image: "Painter.png",
tagKeys: ["agent.painter.tag.1", "agent.painter.tag.2", "agent.painter.tag.3"],
tagKeys: [
"agent.officeArtisan.tag.1",
"agent.officeArtisan.tag.2",
"agent.officeArtisan.tag.3",
],
taskKeys: [
{ titleKey: "agent.painter.task.1.title", descKey: "agent.painter.task.1.desc" },
{ titleKey: "agent.painter.task.2.title", descKey: "agent.painter.task.2.desc" },
{ titleKey: "agent.painter.task.3.title", descKey: "agent.painter.task.3.desc" },
{
titleKey: "agent.officeArtisan.task.1.title",
descKey: "agent.officeArtisan.task.1.desc",
},
{
titleKey: "agent.officeArtisan.task.2.title",
descKey: "agent.officeArtisan.task.2.desc",
},
{
titleKey: "agent.officeArtisan.task.3.title",
descKey: "agent.officeArtisan.task.3.desc",
},
],
installedByDefault: false,
skills: catalogSkills(),
workspaceDirName: "workspace-office-artisan",
personaProfile: "office-artisan",
},
{
id: "dr-pulse",
Expand Down Expand Up @@ -315,6 +331,7 @@ export function getAgentOwnedSkillIds(agentId: string): readonly string[] {
export const LEGACY_AGENT_ID_ALIASES: Readonly<Record<string, string>> = {
leopard: "market-sentinel",
master: "dr-pulse",
painter: "office-artisan",
singer: "creative-muse",
};

Expand Down
111 changes: 111 additions & 0 deletions desktop/src/agent-personas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
listConfiguredAgents,
MARKET_SENTINEL_FINANCIAL_BOUNDARY_SECTION,
MARKET_SENTINEL_OPERATING_BOUNDARY_SECTION,
OFFICE_ARTISAN_ARTIFACT_BOUNDARY_SECTION,
removeConfiguredAgent,
resolveAgentPersonaWorkspace,
seedAgentPersonaWorkspaces,
Expand Down Expand Up @@ -260,6 +261,33 @@ describe("agent personas", () => {
);
});

it("seeds the Office Artisan workspace with its artifact-production context", () => {
const stateDir = createStateDir();
const config = { agents: {} };
const persona = getAgentPersona("office-artisan");
if (!persona) throw new Error("Office Artisan persona is not registered");
ensureAgentPersonasConfig(config, stateDir, [...DEFAULT_AGENT_PERSONAS, persona]);
expect(listConfiguredAgents(config)).toContainEqual({
id: "office-artisan",
name: "Office Artisan",
});
const created = seedAgentPersonaWorkspaces(config, stateDir, "## Shared safety rule");
const workspace = getAgentWorkspacePath(stateDir, persona);
if (!workspace) throw new Error("Office Artisan workspace is not configured");

expect(workspace).toBe(path.join(stateDir, "workspace-office-artisan"));
expect(created).toHaveLength(5);
expect(fs.readFileSync(path.join(workspace, "SOUL.md"), "utf-8")).toContain(
"Preserve original files by default",
);
expect(fs.readFileSync(path.join(workspace, "AGENTS.md"), "utf-8")).toContain(
"Re-open or inspect the generated artifact",
);
expect(fs.readFileSync(path.join(workspace, "IDENTITY.md"), "utf-8")).toContain(
"Name: Office Artisan",
);
});

it("seeds the Creative Muse workspace with its platform content context", () => {
const stateDir = createStateDir();
const config = { agents: {} };
Expand Down Expand Up @@ -723,6 +751,70 @@ Keep this section.
expect(seedAgentPersonaWorkspaces(config, stateDir, "## Shared safety rule")).toEqual([]);
});

it("migrates an installed Painter to Office Artisan and preserves its implicit workspace", () => {
const stateDir = createStateDir();
const legacyWorkspace = path.join(stateDir, "workspace-painter");
fs.mkdirSync(legacyWorkspace);
const agentsPath = path.join(legacyWorkspace, "AGENTS.md");
const soulPath = path.join(legacyWorkspace, "SOUL.md");
fs.writeFileSync(agentsPath, "# Custom production guide\n\nKeep my template rules.\n", "utf-8");
fs.writeFileSync(soulPath, "# Custom production persona\n\nKeep my review voice.\n", "utf-8");
const config = {
agents: {
list: [
{ id: "main", name: "Assistant" },
{
id: "painter",
name: "My Document Studio",
skills: ["custom-skill"],
model: "custom/office-model",
default: true,
templatePack: "corporate",
},
],
},
};

expect(ensureAgentPersonasConfig(config, stateDir).changed).toBe(true);
expect(listConfiguredAgents(config)).toEqual([
{ id: "main", name: "Assistant" },
{ id: "office-artisan", name: "My Document Studio" },
]);
expect(agentList(config).find((entry) => entry.id === "office-artisan")).toMatchObject({
name: "My Document Studio",
workspace: legacyWorkspace,
skills: ["custom-skill"],
model: "custom/office-model",
default: true,
templatePack: "corporate",
});
expect(agentList(config).find((entry) => entry.id === "painter")).toMatchObject({
name: "My Document Studio",
workspace: legacyWorkspace,
skills: ["custom-skill"],
model: "custom/office-model",
templatePack: "corporate",
});
expect(agentList(config).find((entry) => entry.id === "painter")).not.toHaveProperty("default");

expect(seedAgentPersonaWorkspaces(config, stateDir, "## Shared safety rule")).toEqual([
path.join(stateDir, "workspace-main", "IDENTITY.md"),
path.join(stateDir, "workspace-main", "SOUL.md"),
agentsPath,
path.join(legacyWorkspace, "IDENTITY.md"),
soulPath,
]);
const agents = fs.readFileSync(agentsPath, "utf-8");
const soul = fs.readFileSync(soulPath, "utf-8");
expect(agents).toContain("Keep my template rules.");
expect(agents).toContain(OFFICE_ARTISAN_ARTIFACT_BOUNDARY_SECTION.split("\n")[0]);
expect(soul).toContain("Keep my review voice.");
expect(soul).toContain(OFFICE_ARTISAN_ARTIFACT_BOUNDARY_SECTION.split("\n")[0]);
expect(soul).toContain("## Shared safety rule");
expect(seedAgentPersonaWorkspaces(config, stateDir, "## Shared safety rule")).toEqual([]);
expect(fs.existsSync(path.join(stateDir, "workspace-office-artisan"))).toBe(false);
});

it("keeps Singer's implicit workspace when migrating to Creative Muse", () => {
const stateDir = createStateDir();
const config = {
Expand Down Expand Up @@ -943,6 +1035,25 @@ _Fill this in during your first conversation. Make it yours._
expect(config.agents.list).toEqual([{ id: "main", name: "Assistant", default: true }]);
});

it("removes Office Artisan and its Painter compatibility alias together", () => {
const config = {
agents: {
list: [
{ id: "main", name: "Assistant", default: true },
{
id: "office-artisan",
name: "Office Artisan",
workspace: "workspace-painter",
},
{ id: "painter", name: "Office Artisan", workspace: "workspace-painter" },
],
},
};

expect(removeConfiguredAgent(config, "office-artisan").changed).toBe(true);
expect(config.agents.list).toEqual([{ id: "main", name: "Assistant", default: true }]);
});

it("does not allow the built-in default agent to be removed", () => {
const config = {
agents: {
Expand Down
Loading
Loading