From 36fb088b93459073b04fb7ab92a1abd73b246b1a Mon Sep 17 00:00:00 2001 From: xierfloat <2053619887@qq.com> Date: Sun, 21 Dec 2025 12:11:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=E8=B5=84=E6=BA=90=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E9=A2=84=E8=A7=88=E6=A8=A1=E5=BC=8F=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=A4=8D=E5=88=B6=E6=8F=90=E7=A4=BA=E8=AF=8D=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 预览模式下显示"复制提示词"按钮替代"保存文件"按钮 - 点击复制预览内容到剪贴板并显示成功提示 - 修复预览内容滚动问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/copy-prompt-button.md | 10 ++ apps/desktop/src/i18n/locales/en.json | 13 +- apps/desktop/src/i18n/locales/zh-CN.json | 13 +- apps/desktop/src/main/i18n/en.json | 6 + apps/desktop/src/main/i18n/zh-CN.json | 6 + .../src/main/windows/ResourceListWindow.ts | 48 ++++++ .../components/ResourceEditor.tsx | 163 ++++++++++++++---- 7 files changed, 219 insertions(+), 40 deletions(-) create mode 100644 .changeset/copy-prompt-button.md diff --git a/.changeset/copy-prompt-button.md b/.changeset/copy-prompt-button.md new file mode 100644 index 00000000..13dcc9c8 --- /dev/null +++ b/.changeset/copy-prompt-button.md @@ -0,0 +1,10 @@ +--- +"@promptx/desktop": patch +--- + +feat: 资源编辑器预览模式添加复制提示词按钮 + +- 在资源编辑器的预览标签页中,将原本的"保存文件"按钮替换为"复制提示词"按钮 +- 点击按钮可将预览的完整提示词内容复制到剪贴板,并显示成功提示 +- 修复预览内容区域无法滚动的问题,通过在多层 flex 容器中添加 min-h-0 解决 +- 新增中英文翻译:copyPrompt(复制提示词)、copySuccess(复制成功提示) diff --git a/apps/desktop/src/i18n/locales/en.json b/apps/desktop/src/i18n/locales/en.json index a341a1ac..a10d48c2 100644 --- a/apps/desktop/src/i18n/locales/en.json +++ b/apps/desktop/src/i18n/locales/en.json @@ -177,7 +177,9 @@ "saveResourceInfo": "Save Resource Info", "saveFile": "Save File", "close": "Close", - "saving": "Saving..." + "saving": "Saving...", + "copyPrompt": "Copy Prompt", + "copySuccess": "Prompt copied to clipboard" }, "messages": { "loadFilesFailed": "Failed to load file list", @@ -191,6 +193,15 @@ "loading": "Loading...", "loadingFileContent": "Loading file content..." }, + "tabs": { + "editor": "Editor", + "preview": "Preview" + }, + "preview": { + "loading": "Loading preview...", + "empty": "No preview content", + "failed": "Failed to load preview" + }, "readOnly": "⚠️ This resource is in read-only mode ({{source}})" } }, diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json index 7ba99d96..de3d2be2 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.json +++ b/apps/desktop/src/i18n/locales/zh-CN.json @@ -177,7 +177,9 @@ "saveResourceInfo": "保存资源信息", "saveFile": "保存文件", "close": "关闭", - "saving": "保存中..." + "saving": "保存中...", + "copyPrompt": "复制提示词", + "copySuccess": "提示词已复制到剪贴板" }, "messages": { "loadFilesFailed": "加载文件列表失败", @@ -191,6 +193,15 @@ "loading": "加载中...", "loadingFileContent": "正在加载文件内容..." }, + "tabs": { + "editor": "编辑器", + "preview": "预览" + }, + "preview": { + "loading": "正在加载预览...", + "empty": "暂无预览内容", + "failed": "预览加载失败" + }, "readOnly": "⚠️ 此资源为只读模式({{source}})" } }, diff --git a/apps/desktop/src/main/i18n/en.json b/apps/desktop/src/main/i18n/en.json index ccba6515..f28635ea 100644 --- a/apps/desktop/src/main/i18n/en.json +++ b/apps/desktop/src/main/i18n/en.json @@ -87,6 +87,12 @@ "mismatchToolExpectedRole": "Type mismatch: selected 'tool' but the archive contains a role resource", "validationFailed": "Failed to validate resource type" } + }, + "preview": { + "onlyRoleSupported": "Only role resources support preview", + "resolveFailed": "Failed to resolve resource", + "failed": "Failed to generate preview", + "empty": "Preview content is empty" } }, "errors": { diff --git a/apps/desktop/src/main/i18n/zh-CN.json b/apps/desktop/src/main/i18n/zh-CN.json index a6837de9..dcac32ee 100644 --- a/apps/desktop/src/main/i18n/zh-CN.json +++ b/apps/desktop/src/main/i18n/zh-CN.json @@ -87,6 +87,12 @@ "mismatchToolExpectedRole": "类型不匹配:选择了工具资源但压缩包中是角色资源", "validationFailed": "验证资源类型失败" } + }, + "preview": { + "onlyRoleSupported": "仅支持角色资源预览", + "resolveFailed": "资源解析失败", + "failed": "预览生成失败", + "empty": "预览内容为空" } }, "errors": { diff --git a/apps/desktop/src/main/windows/ResourceListWindow.ts b/apps/desktop/src/main/windows/ResourceListWindow.ts index ee4d9564..0f9c82e1 100644 --- a/apps/desktop/src/main/windows/ResourceListWindow.ts +++ b/apps/desktop/src/main/windows/ResourceListWindow.ts @@ -621,6 +621,54 @@ export class ResourceListWindow { return { success: false, message: error?.message || t('resources.importFailed') } } }) + + // 预览完整提示词(DPML -> Prompt) + ipcMain.handle('resources:previewPrompt', async (_evt, payload: { + id: string + type: 'role' | 'tool' + source: string + }) => { + try { + const { id, type, source } = payload || {} + + if (!id || !type) { + return { success: false, message: t('resources.missingParams') } + } + + // 只支持角色类型的预览 + if (type !== 'role') { + return { success: false, message: t('resources.preview.onlyRoleSupported') } + } + + // 使用 CLI 执行 action 命令(与 MCP action 工具相同的方式) + const core = await import('@promptx/core') + const coreExports = (core as any).default || core + const cli = (coreExports as any).cli || (coreExports as any).pouch?.cli + + if (!cli || !cli.execute) { + return { success: false, message: 'CLI not available in @promptx/core' } + } + + // 执行 action 命令获取渲染后的提示词 + const result = await cli.execute('action', [id]) + + // result 包含渲染后的完整提示词 + if (result && typeof result === 'string') { + return { success: true, prompt: result } + } else if (result && result.output) { + return { success: true, prompt: result.output } + } else if (result && result.content) { + return { success: true, prompt: result.content } + } else { + // 尝试将结果转为字符串 + const promptText = String(result || '') + return { success: true, prompt: promptText || t('resources.preview.empty') } + } + } catch (error: any) { + console.error('Failed to preview prompt:', error) + return { success: false, message: error?.message || t('resources.preview.failed') } + } + }) } show(): void { diff --git a/apps/desktop/src/view/pages/resources-window/components/ResourceEditor.tsx b/apps/desktop/src/view/pages/resources-window/components/ResourceEditor.tsx index ec585673..b652cfb9 100644 --- a/apps/desktop/src/view/pages/resources-window/components/ResourceEditor.tsx +++ b/apps/desktop/src/view/pages/resources-window/components/ResourceEditor.tsx @@ -2,8 +2,10 @@ import { useState, useEffect } from "react" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { toast } from "sonner" import { useTranslation } from "react-i18next" +import { Eye, FileCode, Copy } from "lucide-react" type ResourceItem = { id: string @@ -34,6 +36,11 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc const [editingDescription, setEditingDescription] = useState("") const [resourceInfoChanged, setResourceInfoChanged] = useState(false) + // 预览状态 + const [activeTab, setActiveTab] = useState<"editor" | "preview">("editor") + const [previewContent, setPreviewContent] = useState("") + const [previewLoading, setPreviewLoading] = useState(false) + // 当编辑项改变时,初始化状态 useEffect(() => { if (editingItem && isOpen) { @@ -155,6 +162,40 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc } } + const loadPreview = async () => { + if (!editingItem || editingItem.type !== 'role') return + + setPreviewLoading(true) + setPreviewContent("") + + try { + const result = await window.electronAPI?.invoke("resources:previewPrompt", { + id: editingItem.id, + type: editingItem.type, + source: editingItem.source ?? "user" + }) + + if (result?.success) { + setPreviewContent(result.prompt || "") + } else { + setPreviewContent(`⚠️ ${result?.message || t("resources.editor.preview.failed")}`) + } + } catch (error: any) { + console.error("Failed to load preview:", error) + setPreviewContent(`⚠️ ${error?.message || t("resources.editor.preview.failed")}`) + } finally { + setPreviewLoading(false) + } + } + + // 切换到预览标签时加载预览 + const handleTabChange = (tab: string) => { + setActiveTab(tab as "editor" | "preview") + if (tab === "preview" && editingItem?.type === "role") { + loadPreview() + } + } + const handleClose = () => { setFileList([]) setSelectedFile(null) @@ -165,6 +206,8 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc setEditingName("") setEditingDescription("") setResourceInfoChanged(false) + setActiveTab("editor") + setPreviewContent("") onClose() } @@ -224,7 +267,7 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc {/* 弹窗内容 */} -
+
{/* 左侧文件列表 */}

{t("resources.editor.fileList")}

@@ -261,45 +304,89 @@ export default function ResourceEditor({ isOpen, onClose, editingItem, onResourc
- {/* 右侧编辑器 */} -
- {/* 编辑器头部 */} -
- - {selectedFile ? t("resources.editor.editFile", { file: selectedFile }) : t("resources.editor.selectFile")} - - {selectedFile && ( - - )} -
+ {/* 右侧内容区域 */} +
+ + {/* 标签页头部 */} +
+ + + + {t("resources.editor.tabs.editor")} + + {editingItem?.type === "role" && ( + + + {t("resources.editor.tabs.preview")} + + )} + - {/* 编辑器内容 */} -
- {fileContentLoading ? ( -
-

{t("resources.editor.messages.loadingFileContent")}

-
- ) : selectedFile ? ( -