diff --git a/.changeset/witty-ideas-flow.md b/.changeset/witty-ideas-flow.md new file mode 100644 index 00000000..3bba43a5 --- /dev/null +++ b/.changeset/witty-ideas-flow.md @@ -0,0 +1,15 @@ +--- +"@promptx/desktop": patch +--- +feat: 角色/工具详情面板添加导出按钮,支持 v1 和 v2 角色导出 + +- 角色和工具详情面板右上角新增导出按钮(非 system 资源可见) +- 后端 resources:download 支持 version 参数,v2 角色正确定位 ~/.rolex/roles/ 目录 +- v2 角色导出的 ZIP 以 roleId 为顶层目录,确保导入时还原正确 ID +- 添加 i18n 键:export / exportSuccess / exportFailed(中英文) + +fix: macOS 上 AgentX 对话时子进程不再显示 Dock 图标 + +- macOS 启动时检测 Electron Helper 二进制(LSUIElement=true),用于 spawn 子进程 +- buildOptions 和 AgentXService 的 MCP server 在 macOS 上优先使用 Helper 二进制 +- 所有 spawn 调用添加 windowsHide: true diff --git a/apps/desktop/src/i18n/locales/en.json b/apps/desktop/src/i18n/locales/en.json index 2c4f3112..cadab941 100644 --- a/apps/desktop/src/i18n/locales/en.json +++ b/apps/desktop/src/i18n/locales/en.json @@ -280,7 +280,10 @@ "edit": "Edit", "download": "Download", "delete": "Delete", - "use": "Use" + "use": "Use", + "export": "Export", + "exportSuccess": "Exported successfully", + "exportFailed": "Export failed" }, "messages": { "loadFailed": "Failed to load resources", diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json index 37ebf86b..fee51d7a 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.json +++ b/apps/desktop/src/i18n/locales/zh-CN.json @@ -279,7 +279,10 @@ "edit": "编辑", "download": "下载", "delete": "删除", - "use": "使用" + "use": "使用", + "export": "导出", + "exportSuccess": "导出成功", + "exportFailed": "导出失败" }, "messages": { "loadFailed": "加载资源失败", diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 249f4f40..4b97f6a4 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -217,6 +217,30 @@ class PromptXDesktopApp { if (process.platform === 'win32') { this.ensureWindowsToolsInPath() } + + // On macOS: detect Electron Helper binary to avoid Dock icon flicker + // The Helper binary has LSUIElement=true in its Info.plist, so macOS won't + // show a Dock icon when it's spawned as a child process. + if (process.platform === 'darwin') { + this.detectMacHelperBinary() + } + } + + private detectMacHelperBinary(): void { + const appName = path.basename(process.execPath) + const helperPath = path.join( + path.dirname(process.execPath), + '..', 'Frameworks', + `${appName} Helper.app`, + 'Contents', 'MacOS', + `${appName} Helper` + ) + if (fs.existsSync(helperPath)) { + process.env.PROMPTX_MAC_HELPER_PATH = helperPath + logger.info(`macOS Helper binary detected: ${helperPath}`) + } else { + logger.info('macOS Helper binary not found, using main binary for subprocesses') + } } private ensureWindowsToolsInPath(): void { diff --git a/apps/desktop/src/main/infrastructure/PromptXActivationAdapter.ts b/apps/desktop/src/main/infrastructure/PromptXActivationAdapter.ts index 2461f41c..97b93e69 100644 --- a/apps/desktop/src/main/infrastructure/PromptXActivationAdapter.ts +++ b/apps/desktop/src/main/infrastructure/PromptXActivationAdapter.ts @@ -13,7 +13,7 @@ export class PromptXActivationAdapter implements ActivationAdapter { async activate(role: Role): Promise { try { // 调用promptx action命令激活角色 - const { stdout } = await execAsync(`promptx action ${role.id}`) + const { stdout } = await execAsync(`promptx action ${role.id}`, { windowsHide: true }) // 检查输出判断是否成功 const success = stdout.includes('角色已激活') || diff --git a/apps/desktop/src/main/services/AgentXService.ts b/apps/desktop/src/main/services/AgentXService.ts index c16ce0d9..ef030750 100644 --- a/apps/desktop/src/main/services/AgentXService.ts +++ b/apps/desktop/src/main/services/AgentXService.ts @@ -188,9 +188,11 @@ export class AgentXService { // Add built-in mcp-office server // Use Electron's built-in Node.js (ELECTRON_RUN_AS_NODE=1) so it works // even if the user doesn't have Node.js installed on their system. + // On macOS, prefer the Helper binary to avoid Dock icon flicker. if (mcpOfficePath) { + const mcpCommand = process.env.PROMPTX_MAC_HELPER_PATH || process.execPath mcpServers['mcp-office'] = { - command: process.execPath, + command: mcpCommand, args: [mcpOfficePath], env: { ...process.env, diff --git a/apps/desktop/src/main/windows/ResourceListWindow.ts b/apps/desktop/src/main/windows/ResourceListWindow.ts index bec6c4ef..71006644 100644 --- a/apps/desktop/src/main/windows/ResourceListWindow.ts +++ b/apps/desktop/src/main/windows/ResourceListWindow.ts @@ -306,11 +306,12 @@ export class ResourceListWindow { }) // 新增:下载资源(分享即下载,导出为 ZIP 压缩包) - ipcMain.handle('resources:download', async (_evt, payload: { id: string; type: 'role' | 'tool'; source?: string }) => { + ipcMain.handle('resources:download', async (_evt, payload: { id: string; type: 'role' | 'tool'; source?: string; version?: string }) => { try { const id = payload?.id const type = payload?.type const source = payload?.source ?? 'user' + const version = payload?.version ?? 'v1' if (!id || !type) { return { success: false, message: t('resources.missingParams') } } @@ -324,7 +325,10 @@ export class ResourceListWindow { // 定位资源目录 let sourceDir: string | null = null if (source === 'user') { - sourceDir = path.join(os.homedir(), '.promptx', 'resource', type, id) + // V2 角色存储在 ~/.rolex/roles//,V1 及工具存储在 ~/.promptx/resource/// + sourceDir = (type === 'role' && version === 'v2') + ? path.join(os.homedir(), '.rolex', 'roles', id) + : path.join(os.homedir(), '.promptx', 'resource', type, id) } else if (source === 'project') { try { const { ProjectPathResolver } = require('@promptx/core') @@ -386,7 +390,9 @@ export class ResourceListWindow { } } - await addDirectoryToZip(sourceDir) + // V2 角色导出时用 roleId 作为 ZIP 内顶层目录,确保导入时能正确还原 ID + const zipRootPrefix = (type === 'role' && version === 'v2') ? id : '' + await addDirectoryToZip(sourceDir, zipRootPrefix) // 写入 ZIP 文件 zip.writeZip(zipFilePath) diff --git a/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx b/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx index 7183c3ea..5d714c89 100644 --- a/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx +++ b/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx @@ -9,7 +9,8 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog" -import { Pencil, BookOpen, Layers, Brain, FileText, ChevronRight, ChevronDown, Save, Loader2, Target, Building2, Upload, Trash2 } from "lucide-react" +import { Pencil, BookOpen, Layers, Brain, FileText, ChevronRight, ChevronDown, Save, Loader2, Target, Building2, Upload, Trash2, Download } from "lucide-react" +import { toast } from "sonner" import { useEffect, useState, useCallback } from "react" import type { RoleItem } from "./RoleListPanel" import MemoryTab from "./MemoryTab" @@ -982,6 +983,23 @@ export default function RoleDetailPanel({ selectedRole, onActivate, onDelete, on
+ {(selectedRole.source ?? "user") !== "system" && ( + + )} {(selectedRole.source ?? "user") !== "system" && (
{(selectedTool.source ?? "user") === "user" && (
+