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
15 changes: 15 additions & 0 deletions .changeset/witty-ideas-flow.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion apps/desktop/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,10 @@
"edit": "编辑",
"download": "下载",
"delete": "删除",
"use": "使用"
"use": "使用",
"export": "导出",
"exportSuccess": "导出成功",
"exportFailed": "导出失败"
},
"messages": {
"loadFailed": "加载资源失败",
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class PromptXActivationAdapter implements ActivationAdapter {
async activate(role: Role): Promise<ActivationResult> {
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('角色已激活') ||
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/main/services/AgentXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions apps/desktop/src/main/windows/ResourceListWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') }
}
Expand All @@ -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/<id>/,V1 及工具存储在 ~/.promptx/resource/<type>/<id>/
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')
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -982,6 +983,23 @@ export default function RoleDetailPanel({ selectedRole, onActivate, onDelete, on
</div>
</div>
<div className="flex items-center gap-2">
{(selectedRole.source ?? "user") !== "system" && (
<Button variant="outline" size="sm" onClick={async () => {
try {
const result = await window.electronAPI?.invoke("resources:download", { id: selectedRole.id, type: "role", source: selectedRole.source ?? "user", version: selectedRole.version ?? "v1" })
if (result?.success) {
toast.success(t("resources.actions.exportSuccess"))
} else if (result?.message) {
toast.error(result.message)
}
} catch {
toast.error(t("resources.actions.exportFailed"))
}
}}>
<Download className="h-3.5 w-3.5 mr-1.5" />
{t("resources.actions.export")}
</Button>
)}
{(selectedRole.source ?? "user") !== "system" && (
<Button variant="outline" size="sm" className="text-destructive hover:text-destructive hover:bg-destructive/10 border-destructive/30" onClick={() => onDelete(selectedRole)}>
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"
import { Button } from "@/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { toast } from "sonner"
import { Trash2, Settings2 } from "lucide-react"
import { Trash2, Settings2, Download } from "lucide-react"
import EditInfoDialog from "./EditInfoDialog"
import ToolOverviewTab from "./ToolOverviewTab"
import ToolTestTab from "./ToolTestTab"
Expand Down Expand Up @@ -156,6 +156,21 @@ export default function ToolDetailPanel({ selectedTool, onToolUpdated, onDelete,
</div>
{(selectedTool.source ?? "user") === "user" && (
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={async () => {
try {
const result = await window.electronAPI?.invoke("resources:download", { id: selectedTool.id, type: "tool", source: selectedTool.source ?? "user" })
if (result?.success) {
toast.success(t("resources.actions.exportSuccess"))
} else if (result?.message) {
toast.error(result.message)
}
} catch {
toast.error(t("resources.actions.exportFailed"))
}
}}>
<Download className="h-3.5 w-3.5 mr-1.5" />
{t("resources.actions.export")}
</Button>
<Button variant="outline" size="sm" onClick={() => setShowEditInfo(true)}>
<Settings2 className="h-3.5 w-3.5 mr-1.5" />
{t("tools.detail.editInfo")}
Expand Down
10 changes: 9 additions & 1 deletion packages/runtime/src/environment/buildOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,22 @@ export function buildOptions(
// In packaged Electron apps, 'node' is often not in the system PATH on customer
// machines. process.execPath is always available and with ELECTRON_RUN_AS_NODE=1
// the Electron binary runs as a standard Node.js runtime.
// On macOS, prefer the Electron Helper binary (LSUIElement=true) to avoid Dock icon flicker.
options.spawnClaudeCodeProcess = (spawnOptions) => {
const childProcess = spawn(process.execPath, spawnOptions.args, {
const macHelperPath = process.env.PROMPTX_MAC_HELPER_PATH;
const command =
process.platform === "darwin" && macHelperPath
? macHelperPath
: process.execPath;

const childProcess = spawn(command, spawnOptions.args, {
cwd: spawnOptions.cwd,
env: {
...spawnOptions.env,
ELECTRON_RUN_AS_NODE: "1",
},
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
return childProcess as any;
};
Expand Down