From 4d677fed570921e725dc5539507ec004969532dd Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 11 Jul 2026 10:25:07 +0000 Subject: [PATCH 01/14] fix(web-ui): tighten config sidebar meta copy --- web-ui/modules/i18n/locales/en.mjs | 8 ++++---- web-ui/modules/i18n/locales/ja.mjs | 8 ++++---- web-ui/modules/i18n/locales/vi.mjs | 8 ++++---- web-ui/modules/i18n/locales/zh-tw.mjs | 8 ++++---- web-ui/modules/i18n/locales/zh.mjs | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 51dc1036..95f8bb9f 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -167,13 +167,13 @@ const en = Object.freeze({ 'side.update.currentOnly': 'Current v{current}', 'side.update.retry': 'Retry version check', 'side.config.codex': 'Codex', - 'side.config.codex.meta': 'Provider / Model', + 'side.config.codex.meta': 'Provider · Model', 'side.config.claude': 'Claude Code', - 'side.config.claude.meta': 'Claude Settings', + 'side.config.claude.meta': 'Settings', 'side.config.openclaw': 'OpenClaw', - 'side.config.openclaw.meta': 'JSON5 / AGENTS', + 'side.config.openclaw.meta': 'JSON5 · AGENTS', 'side.config.opencode': 'OpenCode', - 'side.config.opencode.meta': 'Config / Provider', + 'side.config.opencode.meta': 'Provider', 'side.sessions.browser': 'Session Browser', 'side.sessions.browser.meta': 'Browse / Export / Cleanup', 'side.prompts.agents': 'AGENTS.md', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index b3624b30..e4d4d68e 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -168,13 +168,13 @@ const ja = Object.freeze({ 'side.update.currentOnly': '現在 v{current}', 'side.update.retry': 'バージョン確認を再試行', 'side.config.codex': 'Codex', - 'side.config.codex.meta': 'Provider / Model', + 'side.config.codex.meta': 'Provider · Model', 'side.config.claude': 'Claude Code', - 'side.config.claude.meta': 'Claude Settings', + 'side.config.claude.meta': 'Settings', 'side.config.openclaw': 'OpenClaw', - 'side.config.openclaw.meta': 'JSON5 / AGENTS', + 'side.config.openclaw.meta': 'JSON5 · AGENTS', 'side.config.opencode': 'OpenCode', - 'side.config.opencode.meta': 'Config / Provider', + 'side.config.opencode.meta': 'Provider', 'side.sessions.browser': 'セッション閲覧', 'side.sessions.browser.meta': '閲覧 / エクスポート / クリーンアップ', 'side.prompts.agents': 'AGENTS.md', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index 9dcc1995..c216d250 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -128,13 +128,13 @@ const vi = Object.freeze({ 'side.update.currentOnly': 'Hiện tại v{current}', 'side.update.retry': 'Thử kiểm tra phiên bản lại', 'side.config.codex': 'Codex', - 'side.config.codex.meta': 'Provider / Model', + 'side.config.codex.meta': 'Provider · Model', 'side.config.claude': 'Claude Code', - 'side.config.claude.meta': 'Cài đặt Claude', + 'side.config.claude.meta': 'Settings', 'side.config.openclaw': 'OpenClaw', - 'side.config.openclaw.meta': 'JSON5 / AGENTS', + 'side.config.openclaw.meta': 'JSON5 · AGENTS', 'side.config.opencode': 'OpenCode', - 'side.config.opencode.meta': 'Config / Provider', + 'side.config.opencode.meta': 'Provider', 'side.sessions.browser': 'Trình duyệt phiên', 'side.sessions.browser.meta': 'Duyệt / Xuất / Dọn dẹp', 'side.prompts.agents': 'AGENTS.md', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index cb523ca2..1602c9ee 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -167,13 +167,13 @@ const zhTw = Object.freeze({ 'side.update.currentOnly': '目前 v{current}', 'side.update.retry': '重試版本檢查', 'side.config.codex': 'Codex', - 'side.config.codex.meta': 'Provider / Model', + 'side.config.codex.meta': 'Provider · Model', 'side.config.claude': 'Claude Code', - 'side.config.claude.meta': 'Claude Settings', + 'side.config.claude.meta': 'Settings', 'side.config.openclaw': 'OpenClaw', - 'side.config.openclaw.meta': 'JSON5 / AGENTS', + 'side.config.openclaw.meta': 'JSON5 · AGENTS', 'side.config.opencode': 'OpenCode', - 'side.config.opencode.meta': '設定 / Provider', + 'side.config.opencode.meta': 'Provider', 'side.sessions.browser': '會話瀏覽', 'side.sessions.browser.meta': '瀏覽 / 匯出 / 清理', 'side.prompts.agents': 'AGENTS.md', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index da0db376..b1782b45 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -167,13 +167,13 @@ const zh = Object.freeze({ 'side.update.currentOnly': '当前 v{current}', 'side.update.retry': '重试版本检查', 'side.config.codex': 'Codex', - 'side.config.codex.meta': 'Provider / Model', + 'side.config.codex.meta': 'Provider · Model', 'side.config.claude': 'Claude Code', - 'side.config.claude.meta': 'Claude Settings', + 'side.config.claude.meta': 'Settings', 'side.config.openclaw': 'OpenClaw', - 'side.config.openclaw.meta': 'JSON5 / AGENTS', + 'side.config.openclaw.meta': 'JSON5 · AGENTS', 'side.config.opencode': 'OpenCode', - 'side.config.opencode.meta': '配置 / Provider', + 'side.config.opencode.meta': 'Provider', 'side.sessions.browser': '会话浏览', 'side.sessions.browser.meta': '浏览 / 导出 / 清理', 'side.prompts.agents': 'AGENTS.md', From 8da6e32c4a38271fc22a9328b3167c8fd489377f Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 11 Jul 2026 12:49:16 +0000 Subject: [PATCH 02/14] feat(prompts): add reusable prompt presets --- README.md | 4 +- README.vi.md | 1 + README.zh.md | 4 +- tests/unit/prompt-presets.test.mjs | 146 ++++++ tests/unit/run.mjs | 1 + tests/unit/web-ui-behavior-parity.test.mjs | 23 + web-ui/app.js | 16 +- web-ui/modules/app.methods.agents.mjs | 169 +++++- .../app.methods.web-ui-preferences.mjs | 29 +- web-ui/modules/i18n/locales/en.mjs | 26 + web-ui/modules/i18n/locales/ja.mjs | 26 + web-ui/modules/i18n/locales/vi.mjs | 26 + web-ui/modules/i18n/locales/zh-tw.mjs | 26 + web-ui/modules/i18n/locales/zh.mjs | 26 + web-ui/partials/index/panel-prompts.html | 50 +- web-ui/res/web-ui-render.precompiled.js | 486 +++++++++++------- web-ui/styles/modals-core.css | 75 +++ 17 files changed, 930 insertions(+), 204 deletions(-) create mode 100644 tests/unit/prompt-presets.test.mjs diff --git a/README.md b/README.md index 3f9e8bf4..d9a1ce92 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**: - **OpenCode Provider Control**: Manage OpenCode provider/model selection with a CodexMate-owned provider store under `~/.codexmate`, projecting only the active provider into native OpenCode config to avoid polluting or deleting user-owned settings. - **Provider Health Cleanup**: Probe local Codex and Claude provider routes, surface failed configs in one modal, and bulk-clean selected broken providers without touching healthy or protected entries. - **Skills Marketplace**: A local-first market to share and import skills between different agent apps. -- **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths. +- **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths, plus a shared preset pool for reusable prompts. - **Task Orchestrator**: Plan and execute complex tasks with dependency tracking. --- @@ -80,7 +80,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**: | **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config | | **Provider Health Check** | ✅ | Probe local Codex/Claude provider routes, highlight failed configs, and bulk-remove selected broken providers safely | | **Prompt Templates** | ✅ | Reusable prompt plugins with variables | -| **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect and path switching | +| **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect, path switching, and a shared preset pool. Applying a preset only updates the editor; save manually to write the file. | | **MCP Integration** | ✅ | Expose local tools and resources via MCP stdio | | **Auto Update** | ✅ | Quick update CLI via `codexmate update` | diff --git a/README.vi.md b/README.vi.md index ddfbf777..3ea59828 100644 --- a/README.vi.md +++ b/README.vi.md @@ -67,6 +67,7 @@ Khác với các wrapper đơn giản, Codex Mate hoạt động như một **Ag | **Bridge OpenAI** | ✅ | Chuyển đổi Codex Responses API sang định dạng OpenAI chuẩn | | **Kiểm tra sức khỏe Provider** | ✅ | Kiểm tra route provider Codex/Claude, đánh dấu cấu hình lỗi và xóa hàng loạt các provider hỏng đã chọn một cách an toàn | | **Mẫu Prompt** | ✅ | Plugin prompt tái sử dụng được với hỗ trợ biến | +| **Trình sửa tệp prompt** | ✅ | Chỉnh sửa CLAUDE.md / AGENTS.md và dùng kho preset chung. Áp dụng preset chỉ cập nhật editor; cần lưu thủ công để ghi vào tệp. | | **Tích hợp MCP** | ✅ | Expose tool và resource cục bộ qua MCP stdio | | **Tự động cập nhật** | ✅ | Cập nhật nhanh qua `codexmate update` | diff --git a/README.zh.md b/README.zh.md index f692b554..d5a1b634 100644 --- a/README.zh.md +++ b/README.zh.md @@ -59,7 +59,7 @@ - **OpenCode Provider 控制**:在 `~/.codexmate` 下维护 CodexMate 自有的 OpenCode 多 provider 存储,只将当前选中的 provider 投影到 OpenCode 原生配置,避免污染或误删用户已有配置。 - **Provider 健康清理**:探测本地 Codex 与 Claude provider 路由,在同一个弹窗里集中展示失败配置,并批量清理已选坏配置,不影响正常或受保护项。 - **Skills 市场**:本地优先的市场,支持在不同的智能体应用之间共享和导入 Skills。 -- **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测。 +- **提示词文件编辑器**:统一编辑全局和项目级 `CLAUDE.md` 与 `AGENTS.md`,支持项目路径自动检测,并提供通用预设池复用 prompt。 - **任务编排器**:支持带有依赖跟踪的复杂任务规划与执行。 --- @@ -79,7 +79,7 @@ | **OpenCode Provider 存储** | ✅ | 在 `~/.codexmate` 中保留多个 OpenCode provider,只将当前选中的 provider 投影到 OpenCode 原生配置 | | **Provider 健康检查** | ✅ | 探测本地 Codex/Claude provider 路由,标记失败配置,并安全批量移除已选坏配置 | | **提示词模板** | ✅ | 支持变量的可复用提示词插件 | -| **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测与路径切换 | +| **提示词文件编辑器** | ✅ | 编辑全局和项目级 CLAUDE.md / AGENTS.md,支持自动检测、路径切换与通用预设池。应用预设只更新编辑器,需手动保存才会写入文件。 | | **MCP 集成** | ✅ | 通过 MCP stdio 暴露本地工具与资源 | | **自动更新** | ✅ | 通过 `codexmate update` 快速更新 CLI | diff --git a/tests/unit/prompt-presets.test.mjs b/tests/unit/prompt-presets.test.mjs new file mode 100644 index 00000000..7d01c39e --- /dev/null +++ b/tests/unit/prompt-presets.test.mjs @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict'; +import { createAgentsMethods } from '../../web-ui/modules/app.methods.agents.mjs'; + +function createVm(overrides = {}) { + const apiCalls = []; + const persisted = []; + const messages = []; + const confirms = []; + const methods = createAgentsMethods({ + api: async (action, params = {}) => { + apiCalls.push({ action, params }); + if (action === 'get-claude-md-file') { + return { content: 'loaded claude', path: '/tmp/CLAUDE.md', exists: true, lineEnding: '\n' }; + } + if (action === 'get-agents-file') { + return { content: 'loaded agents', path: '/tmp/AGENTS.md', exists: true, lineEnding: '\n' }; + } + return {}; + }, + apiWithMeta: async () => ({}) + }); + const vm = { + ...methods, + promptPresets: [], + selectedPromptPresetId: '', + promptPresetNameDraft: '', + promptPresetRenameDraft: {}, + promptPresetSaving: false, + agentsContent: '', + agentsOriginalContent: '', + agentsLoading: false, + agentsSaving: false, + agentsDiffVisible: false, + agentsDiffLines: [], + agentsLineEnding: '\n', + agentsPath: '', + agentsExists: false, + agentsContext: 'codex', + promptsSubTab: 'codex', + projectClaudeMdPath: '', + projectPathOptions: [], + projectPathOptionsLoading: false, + __skipNextPromptsSubTabLoad: false, + $nextTick: async () => {}, + t(key, params = {}) { + return Object.entries(params || {}).reduce((text, [name, value]) => text.replace(`{${name}}`, String(value)), key); + }, + showMessage(message, type) { + messages.push({ message, type }); + }, + async persistWebUiPreferences(snapshot) { + persisted.push(snapshot); + }, + async requestConfirmDialog(options) { + confirms.push(options); + return true; + }, + loadProjectPathOptions() {}, + ...overrides + }; + return { vm, apiCalls, persisted, messages, confirms }; +} + +test('prompt presets reject empty name and empty content', async () => { + const { vm, messages, persisted } = createVm({ + promptPresetNameDraft: ' ', + agentsContent: 'usable content' + }); + + await vm.saveCurrentPromptAsPreset(); + assert.equal(messages.at(-1).message, 'prompts.presets.error.emptyName'); + assert.deepEqual(persisted, []); + + vm.promptPresetNameDraft = 'Preset'; + vm.agentsContent = ' '; + await vm.saveCurrentPromptAsPreset(); + assert.equal(messages.at(-1).message, 'prompts.presets.error.emptyContent'); + assert.deepEqual(persisted, []); +}); + +test('prompt presets save new item and overwrite duplicate only after confirmation', async () => { + const { vm, persisted, confirms } = createVm({ + promptPresetNameDraft: 'Base', + agentsContent: 'first' + }); + + await vm.saveCurrentPromptAsPreset(); + assert.equal(vm.promptPresets.length, 1); + assert.equal(vm.promptPresets[0].name, 'Base'); + assert.equal(vm.promptPresets[0].content, 'first'); + assert.equal(persisted.length, 1); + + vm.promptPresetNameDraft = 'Base'; + vm.agentsContent = 'second'; + await vm.saveCurrentPromptAsPreset(); + assert.equal(confirms.length, 1); + assert.equal(confirms[0].title, 'prompts.presets.confirm.overwriteTitle'); + assert.equal(vm.promptPresets.length, 1); + assert.equal(vm.promptPresets[0].content, 'second'); +}); + +test('prompt presets apply to target editor without writing file', async () => { + const { vm, apiCalls, confirms, messages } = createVm({ + promptPresets: [{ id: 'p1', name: 'Base', content: 'preset body', updatedAt: '2026-01-01T00:00:00.000Z' }], + agentsOriginalContent: 'original', + agentsContent: 'dirty' + }); + + await vm.applyPromptPresetToEditor(vm.promptPresets[0], 'claude-project'); + + assert.equal(confirms.length, 1); + assert.equal(vm.promptsSubTab, 'claude-project'); + assert.equal(vm.agentsContent, 'preset body'); + assert.equal(vm.agentsOriginalContent, 'loaded claude'); + assert.equal(vm.selectedPromptPresetId, 'p1'); + assert.equal(messages.at(-1).message, 'prompts.presets.toast.applied'); + assert.deepEqual(apiCalls.map(call => call.action), ['get-claude-md-file']); + assert.ok(!apiCalls.some(call => call.action.startsWith('apply-'))); +}); + +test('prompt presets prevent rename conflicts and delete after confirmation', async () => { + const { vm, persisted, messages, confirms } = createVm({ + promptPresets: [ + { id: 'p1', name: 'One', content: 'one', updatedAt: '2026-01-01T00:00:00.000Z' }, + { id: 'p2', name: 'Two', content: 'two', updatedAt: '2026-01-02T00:00:00.000Z' } + ], + selectedPromptPresetId: 'p1' + }); + + vm.setPromptPresetRenameDraft('p1', 'Two'); + await vm.renamePromptPreset(vm.promptPresets[0]); + assert.equal(messages.at(-1).message, 'prompts.presets.error.duplicateName'); + assert.equal(vm.promptPresets[0].name, 'One'); + + vm.setPromptPresetRenameDraft('p1', 'Renamed'); + await vm.renamePromptPreset(vm.promptPresets[0]); + assert.equal(vm.promptPresets[0].name, 'Renamed'); + assert.equal(messages.at(-1).message, 'prompts.presets.toast.renamed'); + + await vm.deletePromptPreset(vm.promptPresets[0]); + assert.equal(confirms.at(-1).title, 'prompts.presets.confirm.deleteTitle'); + assert.deepEqual(vm.promptPresets.map(preset => preset.id), ['p2']); + assert.equal(vm.selectedPromptPresetId, ''); + assert.equal(messages.at(-1).message, 'prompts.presets.toast.deleted'); + assert.ok(persisted.length >= 2); +}); diff --git a/tests/unit/run.mjs b/tests/unit/run.mjs index 610f4529..28d2bf3b 100644 --- a/tests/unit/run.mjs +++ b/tests/unit/run.mjs @@ -53,6 +53,7 @@ await import(pathToFileURL(path.join(__dirname, 'claude-settings-sync.test.mjs') await import(pathToFileURL(path.join(__dirname, 'unzip-ext.test.mjs'))); await import(pathToFileURL(path.join(__dirname, 'provider-share-command.test.mjs'))); await import(pathToFileURL(path.join(__dirname, 'web-ui-preferences.test.mjs'))); +await import(pathToFileURL(path.join(__dirname, 'prompt-presets.test.mjs'))); await import(pathToFileURL(path.join(__dirname, 'provider-cache-records.test.mjs'))); await import(pathToFileURL(path.join(__dirname, 'providers-validation.test.mjs'))); await import(pathToFileURL(path.join(__dirname, 'provider-default-names.test.mjs'))); diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 771eba9c..54a1338e 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -375,6 +375,12 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'toolConfigPermissions', 'promptsHint', 'promptsSubTab', + '__skipNextPromptsSubTabLoad', + 'promptPresetNameDraft', + 'promptPresetRenameDraft', + 'promptPresetSaving', + 'promptPresets', + 'selectedPromptPresetId', 'projectClaudeMdPath', 'projectPathOptions', 'projectPathOptionsLoading' @@ -421,6 +427,12 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'showEditProviderKey', 'promptsHint', 'promptsSubTab', + '__skipNextPromptsSubTabLoad', + 'promptPresetNameDraft', + 'promptPresetRenameDraft', + 'promptPresetSaving', + 'promptPresets', + 'selectedPromptPresetId', 'projectClaudeMdPath', 'projectPathOptions', 'projectPathOptionsLoading' @@ -643,6 +655,17 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'submitTaskOrchestrationChatMessage', 'appendTaskWorkflowId', 'openClaudeMdEditor', + 'applyPromptPresetToEditor', + 'buildPromptPresetId', + 'deletePromptPreset', + 'findPromptPresetByName', + 'formatPromptPresetTime', + 'getPromptPresetRenameDraft', + 'normalizePromptPresetName', + 'persistPromptPresets', + 'renamePromptPreset', + 'saveCurrentPromptAsPreset', + 'setPromptPresetRenameDraft', 'switchPromptsSubTab', 'loadPromptsContent', 'openSessionLink', diff --git a/web-ui/app.js b/web-ui/app.js index 1889d1ac..243e4d64 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -74,6 +74,12 @@ document.addEventListener('DOMContentLoaded', () => { showAgentsModal: false, promptsSubTab: 'codex', projectClaudeMdPath: '', + promptPresets: [], + selectedPromptPresetId: '', + promptPresetNameDraft: '', + promptPresetRenameDraft: {}, + promptPresetSaving: false, + __skipNextPromptsSubTabLoad: false, projectPathOptions: [], projectPathOptionsLoading: false, showSkillsModal: false, @@ -730,14 +736,20 @@ document.addEventListener('DOMContentLoaded', () => { if (this.promptsSubTab === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading && typeof this.loadProjectPathOptions === 'function') { this.loadProjectPathOptions(); } - this.loadPromptsContent(); + if (this.promptsSubTab !== 'presets') { + this.loadPromptsContent(); + } } }, promptsSubTab(newVal) { if (typeof this.persistWebUiPreferences === 'function') { this.persistWebUiPreferences({ promptsSubTab: newVal }); } - if (this.mainTab === 'prompts' && typeof this.loadPromptsContent === 'function') { + if (this.__skipNextPromptsSubTabLoad) { + this.__skipNextPromptsSubTabLoad = false; + return; + } + if (this.mainTab === 'prompts' && newVal !== 'presets' && typeof this.loadPromptsContent === 'function') { this.loadPromptsContent(); } }, diff --git a/web-ui/modules/app.methods.agents.mjs b/web-ui/modules/app.methods.agents.mjs index ca698995..b3dcc03c 100644 --- a/web-ui/modules/app.methods.agents.mjs +++ b/web-ui/modules/app.methods.agents.mjs @@ -708,8 +708,175 @@ export function createAgentsMethods(options = {}) { } }, + + normalizePromptPresetName(name) { + return typeof name === 'string' ? name.trim() : ''; + }, + buildPromptPresetId() { + const randomPart = Math.random().toString(36).slice(2, 8); + return `prompt-preset-${Date.now()}-${randomPart}`; + }, + getPromptPresetRenameDraft(preset) { + if (!preset || !preset.id) return ''; + if (Object.prototype.hasOwnProperty.call(this.promptPresetRenameDraft || {}, preset.id)) { + return this.promptPresetRenameDraft[preset.id]; + } + return preset.name || ''; + }, + setPromptPresetRenameDraft(id, value) { + if (!id) return; + this.promptPresetRenameDraft = { + ...(this.promptPresetRenameDraft || {}), + [id]: value + }; + }, + formatPromptPresetTime(value) { + if (typeof value !== 'string' || !value) { + return this.t('common.none'); + } + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return value; + } + return date.toLocaleString(); + }, + findPromptPresetByName(name, excludeId = '') { + const normalizedName = this.normalizePromptPresetName(name).toLowerCase(); + return (Array.isArray(this.promptPresets) ? this.promptPresets : []).find((preset) => { + if (!preset || preset.id === excludeId) return false; + return this.normalizePromptPresetName(preset.name).toLowerCase() === normalizedName; + }) || null; + }, + persistPromptPresets() { + if (typeof this.persistWebUiPreferences === 'function') { + this.persistWebUiPreferences({ promptPresets: this.promptPresets }); + } + }, + async saveCurrentPromptAsPreset() { + if (this.promptPresetSaving || this.agentsLoading) return; + const name = this.normalizePromptPresetName(this.promptPresetNameDraft); + const content = typeof this.agentsContent === 'string' ? this.agentsContent : ''; + if (!name) { + this.showMessage(this.t('prompts.presets.error.emptyName'), 'error'); + return; + } + if (!content.trim()) { + this.showMessage(this.t('prompts.presets.error.emptyContent'), 'error'); + return; + } + const existing = this.findPromptPresetByName(name); + if (existing) { + const confirmed = await this.requestConfirmDialog({ + title: this.t('prompts.presets.confirm.overwriteTitle'), + message: this.t('prompts.presets.confirm.overwriteMessage', { name }), + confirmText: this.t('prompts.presets.confirm.overwriteConfirm'), + cancelText: this.t('common.cancel'), + danger: false + }); + if (!confirmed) return; + } + this.promptPresetSaving = true; + try { + const now = new Date().toISOString(); + if (existing) { + this.promptPresets = this.promptPresets.map((preset) => preset.id === existing.id + ? { ...preset, name, content, updatedAt: now } + : preset); + this.selectedPromptPresetId = existing.id; + } else { + const preset = { + id: this.buildPromptPresetId(), + name, + content, + updatedAt: now + }; + this.promptPresets = [preset, ...this.promptPresets]; + this.selectedPromptPresetId = preset.id; + } + this.promptPresetNameDraft = ''; + this.persistPromptPresets(); + this.showMessage(this.t('prompts.presets.toast.saved'), 'success'); + } finally { + this.promptPresetSaving = false; + } + }, + async applyPromptPresetToEditor(preset, target) { + if (!preset || typeof preset.content !== 'string') return; + const normalizedTarget = target === 'claude-project' ? 'claude-project' : 'codex'; + if (this.hasAgentsContentChanged()) { + const confirmed = await this.requestConfirmDialog({ + title: this.t('prompts.presets.confirm.applyTitle'), + message: this.t('prompts.presets.confirm.applyMessage'), + confirmText: this.t('prompts.presets.confirm.applyConfirm'), + cancelText: this.t('common.cancel'), + danger: true + }); + if (!confirmed) return; + } + this.__skipNextPromptsSubTabLoad = true; + this.promptsSubTab = normalizedTarget; + await this.$nextTick(); + await this.loadPromptsContent(); + this.agentsContent = preset.content; + this.onAgentsContentInput(); + this.selectedPromptPresetId = preset.id; + this.showMessage(this.t('prompts.presets.toast.applied'), 'success'); + }, + async renamePromptPreset(preset) { + if (!preset || !preset.id) return; + const name = this.normalizePromptPresetName(this.getPromptPresetRenameDraft(preset)); + if (!name) { + this.showMessage(this.t('prompts.presets.error.emptyName'), 'error'); + return; + } + const existing = this.findPromptPresetByName(name, preset.id); + if (existing) { + this.showMessage(this.t('prompts.presets.error.duplicateName'), 'error'); + return; + } + if (name === preset.name) { + this.showMessage(this.t('toast.noChanges'), 'info'); + return; + } + const now = new Date().toISOString(); + this.promptPresets = this.promptPresets.map((item) => item.id === preset.id + ? { ...item, name, updatedAt: now } + : item); + const drafts = { ...(this.promptPresetRenameDraft || {}) }; + delete drafts[preset.id]; + this.promptPresetRenameDraft = drafts; + this.persistPromptPresets(); + this.showMessage(this.t('prompts.presets.toast.renamed'), 'success'); + }, + async deletePromptPreset(preset) { + if (!preset || !preset.id) return; + const confirmed = await this.requestConfirmDialog({ + title: this.t('prompts.presets.confirm.deleteTitle'), + message: this.t('prompts.presets.confirm.deleteMessage', { name: preset.name || '' }), + confirmText: this.t('common.delete'), + cancelText: this.t('common.cancel'), + danger: true + }); + if (!confirmed) return; + this.promptPresets = this.promptPresets.filter((item) => item.id !== preset.id); + if (this.selectedPromptPresetId === preset.id) { + this.selectedPromptPresetId = ''; + } + const drafts = { ...(this.promptPresetRenameDraft || {}) }; + delete drafts[preset.id]; + this.promptPresetRenameDraft = drafts; + this.persistPromptPresets(); + this.showMessage(this.t('prompts.presets.toast.deleted'), 'success'); + }, switchPromptsSubTab(subTab) { - const normalized = subTab === 'claude-project' ? 'claude-project' : 'codex'; + const normalized = subTab === 'claude-project' || subTab === 'presets' ? subTab : 'codex'; + if (normalized === 'presets') { + this.promptsSubTab = normalized; + this.$nextTick(() => { + document.querySelector('.main-panel')?.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + }); + return; + } if (normalized === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading) { this.loadProjectPathOptions(); } diff --git a/web-ui/modules/app.methods.web-ui-preferences.mjs b/web-ui/modules/app.methods.web-ui-preferences.mjs index 81dfa550..16600244 100644 --- a/web-ui/modules/app.methods.web-ui-preferences.mjs +++ b/web-ui/modules/app.methods.web-ui-preferences.mjs @@ -62,7 +62,27 @@ function normalizeUsageTimeRange(value) { function normalizePromptsSubTab(value) { const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''; - return normalized === 'claude-project' ? 'claude-project' : 'codex'; + if (normalized === 'claude-project' || normalized === 'presets') return normalized; + return 'codex'; +} + +function normalizePromptPresets(value) { + if (!Array.isArray(value)) return []; + const seen = new Set(); + return value + .filter(item => item && typeof item === 'object') + .map((item, index) => { + const id = typeof item.id === 'string' && item.id.trim() + ? item.id.trim() + : `preset-${Date.now()}-${index}`; + const name = typeof item.name === 'string' ? item.name.trim() : ''; + const content = typeof item.content === 'string' ? item.content : ''; + const updatedAt = typeof item.updatedAt === 'string' && item.updatedAt.trim() + ? item.updatedAt.trim() + : new Date(0).toISOString(); + return { id, name, content, updatedAt }; + }) + .filter(item => item.id && item.name && !seen.has(item.id) && (seen.add(item.id), true)); } function normalizeSessionSortMode(value) { @@ -313,6 +333,7 @@ export function createWebUiPreferencesMethods(options = {}) { : this.configTemplateDiffConfirmEnabled !== false, sessionsUsageTimeRange: normalizeUsageTimeRange(hasOwn(source, 'sessionsUsageTimeRange') ? source.sessionsUsageTimeRange : this.sessionsUsageTimeRange), promptsSubTab: normalizePromptsSubTab(hasOwn(source, 'promptsSubTab') ? source.promptsSubTab : this.promptsSubTab), + promptPresets: normalizePromptPresets(hasOwn(source, 'promptPresets') ? source.promptPresets : this.promptPresets), projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : (typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath : ''), @@ -365,6 +386,12 @@ export function createWebUiPreferencesMethods(options = {}) { if (typeof source.promptsSubTab === 'string') { this.promptsSubTab = normalizePromptsSubTab(source.promptsSubTab); } + if (Array.isArray(source.promptPresets)) { + this.promptPresets = normalizePromptPresets(source.promptPresets); + if (this.selectedPromptPresetId && !this.promptPresets.some(preset => preset.id === this.selectedPromptPresetId)) { + this.selectedPromptPresetId = ''; + } + } if (typeof source.projectClaudeMdPath === 'string') { this.projectClaudeMdPath = source.projectClaudeMdPath; } diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 95f8bb9f..0ef2a52c 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -223,6 +223,32 @@ const en = Object.freeze({ 'subtitle.prompts': 'Edit AGENTS.md and CLAUDE.md.', 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', + 'prompts.subTab.presets': 'Preset pool', + 'prompts.presets.title': 'Prompt preset pool', + 'prompts.presets.hint': 'Save reusable prompt content here, then apply it to AGENTS.md or CLAUDE.md. Applying a preset only updates the editor; click Save to write the file.', + 'prompts.presets.namePlaceholder': 'Preset name', + 'prompts.presets.saveCurrent': 'Save current as preset', + 'prompts.presets.empty': 'No presets yet. Open AGENTS.md or CLAUDE.md, enter content, then save it as a preset.', + 'prompts.presets.updatedAt': 'Updated: {time}', + 'prompts.presets.renameAria': 'Rename prompt preset', + 'prompts.presets.applyAgents': 'Apply to AGENTS.md', + 'prompts.presets.applyClaude': 'Apply to CLAUDE.md', + 'prompts.presets.open': 'Open preset pool', + 'prompts.presets.error.emptyName': 'Preset name cannot be empty', + 'prompts.presets.error.emptyContent': 'Preset content cannot be empty', + 'prompts.presets.error.duplicateName': 'A preset with this name already exists', + 'prompts.presets.confirm.overwriteTitle': 'Overwrite preset?', + 'prompts.presets.confirm.overwriteMessage': 'A preset named “{name}” already exists. Overwrite it with the current editor content?', + 'prompts.presets.confirm.overwriteConfirm': 'Overwrite', + 'prompts.presets.confirm.applyTitle': 'Replace current editor content?', + 'prompts.presets.confirm.applyMessage': 'The current editor has unsaved changes. Applying this preset will replace the editor content, but it will not write the file until you click Save.', + 'prompts.presets.confirm.applyConfirm': 'Apply preset', + 'prompts.presets.confirm.deleteTitle': 'Delete preset?', + 'prompts.presets.confirm.deleteMessage': 'Delete preset “{name}”? This cannot be undone.', + 'prompts.presets.toast.saved': 'Preset saved', + 'prompts.presets.toast.applied': 'Preset applied to editor. Click Save to write the file.', + 'prompts.presets.toast.renamed': 'Preset renamed', + 'prompts.presets.toast.deleted': 'Preset deleted', 'prompts.project.pathLabel': 'Project path (optional)', 'prompts.project.selectPlaceholder': '-- Global (~/.claude/CLAUDE.md) --', 'prompts.project.manualPlaceholder': 'Or enter a project root path...', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index e4d4d68e..968c8d2d 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -224,6 +224,32 @@ const ja = Object.freeze({ 'subtitle.prompts': 'AGENTS.md と CLAUDE.md を編集。', 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', + 'prompts.subTab.presets': 'プリセット', + 'prompts.presets.title': 'Prompt 共通プリセット', + 'prompts.presets.hint': '再利用する prompt 内容を保存し、AGENTS.md または CLAUDE.md に適用できます。適用してもエディターだけが更新され、ファイルへ書き込むには保存が必要です。', + 'prompts.presets.namePlaceholder': 'プリセット名', + 'prompts.presets.saveCurrent': '現在の内容をプリセット保存', + 'prompts.presets.empty': 'プリセットはまだありません。AGENTS.md または CLAUDE.md を開いて内容を編集し、プリセットとして保存してください。', + 'prompts.presets.updatedAt': '更新: {time}', + 'prompts.presets.renameAria': 'prompt プリセット名を変更', + 'prompts.presets.applyAgents': 'AGENTS.md に適用', + 'prompts.presets.applyClaude': 'CLAUDE.md に適用', + 'prompts.presets.open': 'プリセットを開く', + 'prompts.presets.error.emptyName': 'プリセット名は空にできません', + 'prompts.presets.error.emptyContent': 'プリセット内容は空にできません', + 'prompts.presets.error.duplicateName': '同じ名前のプリセットが既に存在します', + 'prompts.presets.confirm.overwriteTitle': 'プリセットを上書きしますか?', + 'prompts.presets.confirm.overwriteMessage': '「{name}」というプリセットは既に存在します。現在のエディター内容で上書きしますか?', + 'prompts.presets.confirm.overwriteConfirm': '上書き', + 'prompts.presets.confirm.applyTitle': '現在のエディター内容を置き換えますか?', + 'prompts.presets.confirm.applyMessage': '現在のエディターには未保存の変更があります。プリセットを適用するとエディター内容は置き換わりますが、保存をクリックするまでファイルには書き込まれません。', + 'prompts.presets.confirm.applyConfirm': 'プリセットを適用', + 'prompts.presets.confirm.deleteTitle': 'プリセットを削除しますか?', + 'prompts.presets.confirm.deleteMessage': 'プリセット「{name}」を削除しますか?この操作は元に戻せません。', + 'prompts.presets.toast.saved': 'プリセットを保存しました', + 'prompts.presets.toast.applied': 'プリセットをエディターへ適用しました。ファイルへ書き込むには保存してください。', + 'prompts.presets.toast.renamed': 'プリセット名を変更しました', + 'prompts.presets.toast.deleted': 'プリセットを削除しました', 'prompts.project.pathLabel': 'プロジェクトパス(任意)', 'prompts.project.selectPlaceholder': '-- グローバル (~/.claude/CLAUDE.md) --', 'prompts.project.manualPlaceholder': 'またはプロジェクトルートパスを入力...', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index c216d250..2a22c31e 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -238,6 +238,32 @@ const vi = Object.freeze({ 'subtitle.prompts': 'Chỉnh sửa AGENTS.md và CLAUDE.md.', 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', + 'prompts.subTab.presets': 'Kho preset', + 'prompts.presets.title': 'Kho preset prompt dùng chung', + 'prompts.presets.hint': 'Lưu nội dung prompt tái sử dụng tại đây, rồi áp dụng vào AGENTS.md hoặc CLAUDE.md. Áp dụng preset chỉ cập nhật editor; cần bấm Lưu để ghi tệp.', + 'prompts.presets.namePlaceholder': 'Tên preset', + 'prompts.presets.saveCurrent': 'Lưu nội dung hiện tại thành preset', + 'prompts.presets.empty': 'Chưa có preset. Mở AGENTS.md hoặc CLAUDE.md, nhập nội dung rồi lưu thành preset.', + 'prompts.presets.updatedAt': 'Cập nhật: {time}', + 'prompts.presets.renameAria': 'Đổi tên preset prompt', + 'prompts.presets.applyAgents': 'Áp dụng vào AGENTS.md', + 'prompts.presets.applyClaude': 'Áp dụng vào CLAUDE.md', + 'prompts.presets.open': 'Mở kho preset', + 'prompts.presets.error.emptyName': 'Tên preset không được để trống', + 'prompts.presets.error.emptyContent': 'Nội dung preset không được để trống', + 'prompts.presets.error.duplicateName': 'Đã có preset trùng tên', + 'prompts.presets.confirm.overwriteTitle': 'Ghi đè preset?', + 'prompts.presets.confirm.overwriteMessage': 'Preset “{name}” đã tồn tại. Ghi đè bằng nội dung editor hiện tại?', + 'prompts.presets.confirm.overwriteConfirm': 'Ghi đè', + 'prompts.presets.confirm.applyTitle': 'Thay nội dung editor hiện tại?', + 'prompts.presets.confirm.applyMessage': 'Editor hiện có thay đổi chưa lưu. Áp dụng preset sẽ thay nội dung editor, nhưng chưa ghi tệp cho tới khi bạn bấm Lưu.', + 'prompts.presets.confirm.applyConfirm': 'Áp dụng preset', + 'prompts.presets.confirm.deleteTitle': 'Xóa preset?', + 'prompts.presets.confirm.deleteMessage': 'Xóa preset “{name}”? Không thể hoàn tác.', + 'prompts.presets.toast.saved': 'Đã lưu preset', + 'prompts.presets.toast.applied': 'Đã áp dụng preset vào editor. Bấm Lưu để ghi tệp.', + 'prompts.presets.toast.renamed': 'Đã đổi tên preset', + 'prompts.presets.toast.deleted': 'Đã xóa preset', 'prompts.project.pathLabel': 'Đường dẫn dự án (tùy chọn)', 'prompts.project.selectPlaceholder': '-- Toàn cục (~/.claude/CLAUDE.md) --', 'prompts.project.manualPlaceholder': 'Hoặc nhập đường dẫn gốc dự án...', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 1602c9ee..f3ef0444 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -223,6 +223,32 @@ const zhTw = Object.freeze({ 'subtitle.prompts': '編輯 AGENTS.md 與 CLAUDE.md。', 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', + 'prompts.subTab.presets': '預設池', + 'prompts.presets.title': 'Prompt 通用預設池', + 'prompts.presets.hint': '把可重用的 prompt 內容保存在這裡,再套用到 AGENTS.md 或 CLAUDE.md。套用預設只更新編輯器,仍需點擊保存才會寫入文件。', + 'prompts.presets.namePlaceholder': '預設名稱', + 'prompts.presets.saveCurrent': '保存目前內容為預設', + 'prompts.presets.empty': '暫無預設。先打開 AGENTS.md 或 CLAUDE.md 編輯內容,再保存為預設。', + 'prompts.presets.updatedAt': '更新時間:{time}', + 'prompts.presets.renameAria': '重新命名 prompt 預設', + 'prompts.presets.applyAgents': '套用到 AGENTS.md', + 'prompts.presets.applyClaude': '套用到 CLAUDE.md', + 'prompts.presets.open': '打開預設池', + 'prompts.presets.error.emptyName': '預設名稱不能為空', + 'prompts.presets.error.emptyContent': '預設內容不能為空', + 'prompts.presets.error.duplicateName': '已存在同名預設', + 'prompts.presets.confirm.overwriteTitle': '覆蓋預設?', + 'prompts.presets.confirm.overwriteMessage': '名為「{name}」的預設已存在。要用目前編輯器內容覆蓋它嗎?', + 'prompts.presets.confirm.overwriteConfirm': '覆蓋', + 'prompts.presets.confirm.applyTitle': '替換目前編輯器內容?', + 'prompts.presets.confirm.applyMessage': '目前編輯器存在未保存改動。套用預設會替換編輯器內容,但不會寫入文件,仍需點擊保存。', + 'prompts.presets.confirm.applyConfirm': '套用預設', + 'prompts.presets.confirm.deleteTitle': '刪除預設?', + 'prompts.presets.confirm.deleteMessage': '刪除預設「{name}」?此操作不可復原。', + 'prompts.presets.toast.saved': '預設已保存', + 'prompts.presets.toast.applied': '預設已套用到編輯器,點擊保存後才會寫入文件。', + 'prompts.presets.toast.renamed': '預設已重新命名', + 'prompts.presets.toast.deleted': '預設已刪除', 'prompts.project.pathLabel': '專案路徑(可選)', 'prompts.project.selectPlaceholder': '-- 全域 (~/.claude/CLAUDE.md) --', 'prompts.project.manualPlaceholder': '或手動輸入專案根路徑...', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index b1782b45..c3661ec5 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -223,6 +223,32 @@ const zh = Object.freeze({ 'subtitle.prompts': '编辑 AGENTS.md 与 CLAUDE.md。', 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', + 'prompts.subTab.presets': '预设池', + 'prompts.presets.title': 'Prompt 通用预设池', + 'prompts.presets.hint': '把可复用的 prompt 内容保存在这里,再应用到 AGENTS.md 或 CLAUDE.md。应用预设只更新编辑器,仍需点击保存才会写入文件。', + 'prompts.presets.namePlaceholder': '预设名称', + 'prompts.presets.saveCurrent': '保存当前内容为预设', + 'prompts.presets.empty': '暂无预设。先打开 AGENTS.md 或 CLAUDE.md 编辑内容,再保存为预设。', + 'prompts.presets.updatedAt': '更新时间:{time}', + 'prompts.presets.renameAria': '重命名 prompt 预设', + 'prompts.presets.applyAgents': '应用到 AGENTS.md', + 'prompts.presets.applyClaude': '应用到 CLAUDE.md', + 'prompts.presets.open': '打开预设池', + 'prompts.presets.error.emptyName': '预设名称不能为空', + 'prompts.presets.error.emptyContent': '预设内容不能为空', + 'prompts.presets.error.duplicateName': '已存在同名预设', + 'prompts.presets.confirm.overwriteTitle': '覆盖预设?', + 'prompts.presets.confirm.overwriteMessage': '名为“{name}”的预设已存在。要用当前编辑器内容覆盖它吗?', + 'prompts.presets.confirm.overwriteConfirm': '覆盖', + 'prompts.presets.confirm.applyTitle': '替换当前编辑器内容?', + 'prompts.presets.confirm.applyMessage': '当前编辑器存在未保存改动。应用预设会替换编辑器内容,但不会写入文件,仍需点击保存。', + 'prompts.presets.confirm.applyConfirm': '应用预设', + 'prompts.presets.confirm.deleteTitle': '删除预设?', + 'prompts.presets.confirm.deleteMessage': '删除预设“{name}”?此操作不可撤销。', + 'prompts.presets.toast.saved': '预设已保存', + 'prompts.presets.toast.applied': '预设已应用到编辑器,点击保存后才会写入文件。', + 'prompts.presets.toast.renamed': '预设已重命名', + 'prompts.presets.toast.deleted': '预设已删除', 'prompts.project.pathLabel': '项目路径(可选)', 'prompts.project.selectPlaceholder': '-- 全局 (~/.claude/CLAUDE.md) --', 'prompts.project.manualPlaceholder': '或手动输入项目根路径...', diff --git a/web-ui/partials/index/panel-prompts.html b/web-ui/partials/index/panel-prompts.html index c41e92c1..187a798a 100644 --- a/web-ui/partials/index/panel-prompts.html +++ b/web-ui/partials/index/panel-prompts.html @@ -8,6 +8,7 @@
+
@@ -32,7 +33,53 @@
-
+
+
+
+
{{ t('prompts.presets.title') }}
+
{{ t('prompts.presets.hint') }}
+
+
+
+ + +
+
{{ t('prompts.presets.empty') }}
+
+
+
+ +
{{ t('prompts.presets.updatedAt', { time: formatPromptPresetTime(preset.updatedAt) }) }}
+
{{ preset.content }}
+
+
+ + + + +
+
+
+
+ +
{{ agentsPath || t('common.notLoaded') }} @@ -65,6 +112,7 @@
+ diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 8716d5f0..ce58fb8a 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -6674,7 +6674,14 @@ return function render(_ctx, _cache) { role: "tab", "aria-selected": _ctx.promptsSubTab === 'claude-project', onClick: $event => (_ctx.switchPromptsSubTab('claude-project')) - }, _toDisplayString(_ctx.t('prompts.subTab.project')), 11 /* TEXT, CLASS, PROPS */, ["aria-selected", "onClick"]) + }, _toDisplayString(_ctx.t('prompts.subTab.project')), 11 /* TEXT, CLASS, PROPS */, ["aria-selected", "onClick"]), + _createElementVNode("button", { + type: "button", + class: _normalizeClass(['prompts-md-tab', { active: _ctx.promptsSubTab === 'presets' }]), + role: "tab", + "aria-selected": _ctx.promptsSubTab === 'presets', + onClick: $event => (_ctx.switchPromptsSubTab('presets')) + }, _toDisplayString(_ctx.t('prompts.subTab.presets')), 11 /* TEXT, CLASS, PROPS */, ["aria-selected", "onClick"]) ], 8 /* PROPS */, ["aria-label"]), (_ctx.promptsSubTab === 'claude-project') ? (_openBlock(), _createElementBlock("div", { @@ -6712,103 +6719,104 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true) ])) : _createCommentVNode("v-if", true), - _createElementVNode("div", { class: "prompts-editor" }, [ - _createElementVNode("div", { class: "prompts-editor-toolbar" }, [ - _createElementVNode("div", { class: "form-hint" }, [ - _createTextVNode(_toDisplayString(_ctx.agentsPath || _ctx.t('common.notLoaded')) + " ", 1 /* TEXT */), - (_ctx.agentsPath) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, " (" + _toDisplayString(_ctx.agentsExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave')) + ") ", 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]), - _createElementVNode("div", { class: "prompts-editor-actions" }, [ - _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--secondary" }, [ - _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.exportAgentsContent, - disabled: _ctx.agentsLoading, - title: _ctx.t('modal.agents.export') - }, [ - (_openBlock(), _createElementBlock("svg", { - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" - }, [ - _createElementVNode("path", { d: "M12 3v12M8 11l4 4 4-4M4 17v2h16v-2" }) - ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]), - _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.copyAgentsContent, - disabled: _ctx.agentsLoading, - title: _ctx.t('modal.agents.copy') - }, [ - (_openBlock(), _createElementBlock("svg", { - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" - }, [ - _createElementVNode("rect", { - x: "9", - y: "9", - width: "13", - height: "13", - rx: "2" - }), - _createElementVNode("path", { d: "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" }) - ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]), - _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.pasteAgentsContent, - disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, - title: _ctx.t('common.paste') - }, [ - (_openBlock(), _createElementBlock("svg", { - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" - }, [ - _createElementVNode("rect", { - x: "8", - y: "2", - width: "8", - height: "4", - rx: "1" - }), - _createElementVNode("path", { d: "M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" }) - ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]) + (_ctx.promptsSubTab === 'presets') + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "prompt-presets-panel" + }, [ + _createElementVNode("div", { class: "prompt-presets-header" }, [ + _createElementVNode("div", null, [ + _createElementVNode("div", { class: "prompt-presets-title" }, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "form-hint" }, _toDisplayString(_ctx.t('prompts.presets.hint')), 1 /* TEXT */) + ]) ]), - _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--workflow" }, [ + _createElementVNode("div", { class: "prompt-presets-save-row" }, [ + _withDirectives(_createElementVNode("input", { + type: "text", + class: "form-input", + "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), + placeholder: _ctx.t('prompts.presets.namePlaceholder'), + onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ + [_vModelText, _ctx.promptPresetNameDraft] + ]), _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.loadPromptsContent, - disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, - title: _ctx.t('common.cancel') - }, [ - (_openBlock(), _createElementBlock("svg", { - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" + type: "button", + class: "btn-primary", + onClick: _ctx.saveCurrentPromptAsPreset, + disabled: _ctx.promptPresetSaving || _ctx.agentsLoading + }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + ]), + (!_ctx.promptPresets.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "state-message" + }, _toDisplayString(_ctx.t('prompts.presets.empty')), 1 /* TEXT */)) + : (_openBlock(), _createElementBlock("div", { + key: 1, + class: "prompt-presets-list" }, [ - _createElementVNode("path", { d: "M18 6L6 18M6 6l12 12" }) + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { + return (_openBlock(), _createElementBlock("article", { + key: preset.id, + class: "prompt-preset-card" + }, [ + _createElementVNode("div", { class: "prompt-preset-main" }, [ + _createElementVNode("input", { + type: "text", + class: "form-input prompt-preset-name", + value: _ctx.getPromptPresetRenameDraft(preset), + onInput: $event => (_ctx.setPromptPresetRenameDraft(preset.id, $event.target.value)), + onKeydown: _withKeys(_withModifiers($event => (_ctx.renamePromptPreset(preset)), ["prevent"]), ["enter"]), + "aria-label": _ctx.t('prompts.presets.renameAria') + }, null, 40 /* PROPS, NEED_HYDRATION */, ["value", "onInput", "onKeydown", "aria-label"]), + _createElementVNode("div", { class: "form-hint" }, _toDisplayString(_ctx.t('prompts.presets.updatedAt', { time: _ctx.formatPromptPresetTime(preset.updatedAt) })), 1 /* TEXT */), + _createElementVNode("pre", { class: "prompt-preset-preview" }, _toDisplayString(preset.content), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "prompt-preset-actions" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.renamePromptPreset(preset)) + }, _toDisplayString(_ctx.t('common.save')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'codex')) + }, _toDisplayString(_ctx.t('prompts.presets.applyAgents')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'claude-project')) + }, _toDisplayString(_ctx.t('prompts.presets.applyClaude')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini btn-danger-mini", + onClick: $event => (_ctx.deletePromptPreset(preset)) + }, _toDisplayString(_ctx.t('common.delete')), 9 /* TEXT, PROPS */, ["onClick"]) + ]) + ])) + }), 128 /* KEYED_FRAGMENT */)) ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]), - (_ctx.agentsDiffVisible) - ? (_openBlock(), _createElementBlock("button", { - key: 0, + ])) + : (_openBlock(), _createElementBlock("div", { + key: 2, + class: "prompts-editor" + }, [ + _createElementVNode("div", { class: "prompts-editor-toolbar" }, [ + _createElementVNode("div", { class: "form-hint" }, [ + _createTextVNode(_toDisplayString(_ctx.agentsPath || _ctx.t('common.notLoaded')) + " ", 1 /* TEXT */), + (_ctx.agentsPath) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, " (" + _toDisplayString(_ctx.agentsExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave')) + ") ", 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]), + _createElementVNode("div", { class: "prompts-editor-actions" }, [ + _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--secondary" }, [ + _createElementVNode("button", { class: "btn-mini", - onClick: _ctx.resetAgentsDiffState, - disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, - title: _ctx.t('common.backToEdit') + onClick: _ctx.exportAgentsContent, + disabled: _ctx.agentsLoading, + title: _ctx.t('modal.agents.export') }, [ (_openBlock(), _createElementBlock("svg", { class: "btn-icon-sm", @@ -6817,129 +6825,217 @@ return function render(_ctx, _cache) { stroke: "currentColor", "stroke-width": "2" }, [ - _createElementVNode("path", { d: "M19 12H5M12 19l-7-7 7-7" }) + _createElementVNode("path", { d: "M12 3v12M8 11l4 4 4-4M4 17v2h16v-2" }) ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"])) - : _createCommentVNode("v-if", true), - _createElementVNode("button", { - class: "btn-mini btn-confirm-mini", - onClick: _ctx.applyAgentsContent, - disabled: _ctx.agentsSaving || _ctx.agentsLoading || _ctx.agentsDiffLoading || (!_ctx.agentsDiffVisible && !_ctx.hasAgentsContentChanged()) || (_ctx.agentsDiffVisible && !_ctx.agentsDiffHasChanges), - title: _ctx.agentsDiffVisible ? _ctx.t('common.save') : _ctx.t('common.preview') - }, [ - (_ctx.agentsDiffVisible) - ? (_openBlock(), _createElementBlock("svg", { - key: 0, + ], 8 /* PROPS */, ["onClick", "disabled", "title"]), + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.copyAgentsContent, + disabled: _ctx.agentsLoading, + title: _ctx.t('modal.agents.copy') + }, [ + (_openBlock(), _createElementBlock("svg", { class: "btn-icon-sm", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ - _createElementVNode("path", { d: "M20 6L9 17l-5-5" }) + _createElementVNode("rect", { + x: "9", + y: "9", + width: "13", + height: "13", + rx: "2" + }), + _createElementVNode("path", { d: "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" }) ])) - : (_openBlock(), _createElementBlock("svg", { - key: 1, + ], 8 /* PROPS */, ["onClick", "disabled", "title"]), + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.pasteAgentsContent, + disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + title: _ctx.t('common.paste') + }, [ + (_openBlock(), _createElementBlock("svg", { class: "btn-icon-sm", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ - _createElementVNode("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), - _createElementVNode("circle", { - cx: "12", - cy: "12", - r: "3" - }) + _createElementVNode("rect", { + x: "8", + y: "2", + width: "8", + height: "4", + rx: "1" + }), + _createElementVNode("path", { d: "M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" }) ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]) - ]) - ]) - ]), - _createElementVNode("div", { class: "form-group" }, [ - (_ctx.agentsDiffVisible) - ? (_openBlock(), _createElementBlock("div", { key: 0 }, [ - (!_ctx.agentsDiffLoading && !_ctx.agentsDiffError && !_ctx.agentsDiffTruncated && (_ctx.agentsDiffStats.added || _ctx.agentsDiffStats.removed)) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "agents-diff-summary" + ], 8 /* PROPS */, ["onClick", "disabled", "title"]) + ]), + _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--workflow" }, [ + _createElementVNode("button", { + class: "btn-mini", + onClick: $event => (_ctx.switchPromptsSubTab('presets')), + disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffLoading, + title: _ctx.t('prompts.presets.open') + }, _toDisplayString(_ctx.t('prompts.subTab.presets')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.loadPromptsContent, + disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, + title: _ctx.t('common.cancel') + }, [ + (_openBlock(), _createElementBlock("svg", { + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" }, [ - _createElementVNode("span", { class: "agents-diff-stat add" }, "+" + _toDisplayString(_ctx.agentsDiffStats.added), 1 /* TEXT */), - _createElementVNode("span", { class: "agents-diff-stat del" }, "-" + _toDisplayString(_ctx.agentsDiffStats.removed), 1 /* TEXT */) + _createElementVNode("path", { d: "M18 6L6 18M6 6l12 12" }) ])) - : _createCommentVNode("v-if", true), - (_ctx.agentsDiffLoading) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "state-message" - }, _toDisplayString(_ctx.t('diff.generating')), 1 /* TEXT */)) - : (_ctx.agentsDiffError) - ? (_openBlock(), _createElementBlock("div", { - key: 2, - class: "state-message error" - }, _toDisplayString(_ctx.agentsDiffError), 1 /* TEXT */)) - : (_ctx.agentsDiffTruncated) + ], 8 /* PROPS */, ["onClick", "disabled", "title"]), + (_ctx.agentsDiffVisible) + ? (_openBlock(), _createElementBlock("button", { + key: 0, + class: "btn-mini", + onClick: _ctx.resetAgentsDiffState, + disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, + title: _ctx.t('common.backToEdit') + }, [ + (_openBlock(), _createElementBlock("svg", { + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" + }, [ + _createElementVNode("path", { d: "M19 12H5M12 19l-7-7 7-7" }) + ])) + ], 8 /* PROPS */, ["onClick", "disabled", "title"])) + : _createCommentVNode("v-if", true), + _createElementVNode("button", { + class: "btn-mini btn-confirm-mini", + onClick: _ctx.applyAgentsContent, + disabled: _ctx.agentsSaving || _ctx.agentsLoading || _ctx.agentsDiffLoading || (!_ctx.agentsDiffVisible && !_ctx.hasAgentsContentChanged()) || (_ctx.agentsDiffVisible && !_ctx.agentsDiffHasChanges), + title: _ctx.agentsDiffVisible ? _ctx.t('common.save') : _ctx.t('common.preview') + }, [ + (_ctx.agentsDiffVisible) + ? (_openBlock(), _createElementBlock("svg", { + key: 0, + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" + }, [ + _createElementVNode("path", { d: "M20 6L9 17l-5-5" }) + ])) + : (_openBlock(), _createElementBlock("svg", { + key: 1, + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" + }, [ + _createElementVNode("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), + _createElementVNode("circle", { + cx: "12", + cy: "12", + r: "3" + }) + ])) + ], 8 /* PROPS */, ["onClick", "disabled", "title"]) + ]) + ]) + ]), + _createElementVNode("div", { class: "form-group" }, [ + (_ctx.agentsDiffVisible) + ? (_openBlock(), _createElementBlock("div", { key: 0 }, [ + (!_ctx.agentsDiffLoading && !_ctx.agentsDiffError && !_ctx.agentsDiffTruncated && (_ctx.agentsDiffStats.added || _ctx.agentsDiffStats.removed)) ? (_openBlock(), _createElementBlock("div", { - key: 3, - class: "agents-diff-empty" - }, _toDisplayString(_ctx.t('diff.tooLargeSkip')), 1 /* TEXT */)) - : (!_ctx.agentsDiffHasChanges) + key: 0, + class: "agents-diff-summary" + }, [ + _createElementVNode("span", { class: "agents-diff-stat add" }, "+" + _toDisplayString(_ctx.agentsDiffStats.added), 1 /* TEXT */), + _createElementVNode("span", { class: "agents-diff-stat del" }, "-" + _toDisplayString(_ctx.agentsDiffStats.removed), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true), + (_ctx.agentsDiffLoading) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "state-message" + }, _toDisplayString(_ctx.t('diff.generating')), 1 /* TEXT */)) + : (_ctx.agentsDiffError) ? (_openBlock(), _createElementBlock("div", { - key: 4, - class: "agents-diff-empty" - }, _toDisplayString(_ctx.t('diff.noChanges')), 1 /* TEXT */)) - : (_openBlock(), _createElementBlock("div", { - key: 5, - class: "agents-diff-view agents-diff-editor" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.agentsDiffLines, (line, index) => { - return (_openBlock(), _createElementBlock("div", { - key: line.key || (line.type + '-' + index), - class: _normalizeClass(['agents-diff-line', line.type]) + key: 2, + class: "state-message error" + }, _toDisplayString(_ctx.agentsDiffError), 1 /* TEXT */)) + : (_ctx.agentsDiffTruncated) + ? (_openBlock(), _createElementBlock("div", { + key: 3, + class: "agents-diff-empty" + }, _toDisplayString(_ctx.t('diff.tooLargeSkip')), 1 /* TEXT */)) + : (!_ctx.agentsDiffHasChanges) + ? (_openBlock(), _createElementBlock("div", { + key: 4, + class: "agents-diff-empty" + }, _toDisplayString(_ctx.t('diff.noChanges')), 1 /* TEXT */)) + : (_openBlock(), _createElementBlock("div", { + key: 5, + class: "agents-diff-view agents-diff-editor" }, [ - _createElementVNode("span", { class: "agents-diff-line-sign" }, _toDisplayString(line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ')), 1 /* TEXT */), - _createElementVNode("span", { class: "agents-diff-line-text" }, _toDisplayString(line.value), 1 /* TEXT */) - ], 2 /* CLASS */)) - }), 128 /* KEYED_FRAGMENT */)) - ])) - ])) - : _createCommentVNode("v-if", true), - _createElementVNode("div", { - class: _normalizeClass(['editor-frame', { 'editor-frame--loading': _ctx.agentsLoading }]) - }, [ - (_ctx.agentsLoading) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "editor-skeleton" - }, [ - (_openBlock(), _createElementBlock(_Fragment, null, _renderList(6, (i) => { - return _createElementVNode("div", { - class: "skeleton-line", - key: i - }) - }), 64 /* STABLE_FRAGMENT */)) - ])) - : _createCommentVNode("v-if", true), - _withDirectives(_createElementVNode("textarea", { - "onUpdate:modelValue": $event => ((_ctx.agentsContent) = $event), - class: "form-input template-editor", - spellcheck: "false", - readonly: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, - onInput: _ctx.onAgentsContentInput, - placeholder: _ctx.t(_ctx.promptsSubTab === 'claude-project' ? 'modal.agents.placeholder.claudeProject' : 'modal.agents.placeholder') - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "readonly", "onInput", "placeholder"]), [ - [_vModelText, _ctx.agentsContent] + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.agentsDiffLines, (line, index) => { + return (_openBlock(), _createElementBlock("div", { + key: line.key || (line.type + '-' + index), + class: _normalizeClass(['agents-diff-line', line.type]) + }, [ + _createElementVNode("span", { class: "agents-diff-line-sign" }, _toDisplayString(line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ')), 1 /* TEXT */), + _createElementVNode("span", { class: "agents-diff-line-text" }, _toDisplayString(line.value), 1 /* TEXT */) + ], 2 /* CLASS */)) + }), 128 /* KEYED_FRAGMENT */)) + ])) + ])) + : _createCommentVNode("v-if", true), + _createElementVNode("div", { + class: _normalizeClass(['editor-frame', { 'editor-frame--loading': _ctx.agentsLoading }]) + }, [ + (_ctx.agentsLoading) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "editor-skeleton" + }, [ + (_openBlock(), _createElementBlock(_Fragment, null, _renderList(6, (i) => { + return _createElementVNode("div", { + class: "skeleton-line", + key: i + }) + }), 64 /* STABLE_FRAGMENT */)) + ])) + : _createCommentVNode("v-if", true), + _withDirectives(_createElementVNode("textarea", { + "onUpdate:modelValue": $event => ((_ctx.agentsContent) = $event), + class: "form-input template-editor", + spellcheck: "false", + readonly: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + onInput: _ctx.onAgentsContentInput, + placeholder: _ctx.t(_ctx.promptsSubTab === 'claude-project' ? 'modal.agents.placeholder.claudeProject' : 'modal.agents.placeholder') + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "readonly", "onInput", "placeholder"]), [ + [_vModelText, _ctx.agentsContent] + ]) + ], 2 /* CLASS */), + (_ctx.promptsContextHint) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: _normalizeClass(['prompts-context-hint', { 'prompts-context-hint--warn': _ctx.promptsContextHint.warn }]) + }, _toDisplayString(_ctx.promptsContextHint.text), 3 /* TEXT, CLASS */)) + : _createCommentVNode("v-if", true) ]) - ], 2 /* CLASS */), - (_ctx.promptsContextHint) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: _normalizeClass(['prompts-context-hint', { 'prompts-context-hint--warn': _ctx.promptsContextHint.warn }]) - }, _toDisplayString(_ctx.promptsContextHint.text), 3 /* TEXT, CLASS */)) - : _createCommentVNode("v-if", true) - ]) - ]) + ])) ])) : _createCommentVNode("v-if", true), _createCommentVNode(" 加载状态 "), diff --git a/web-ui/styles/modals-core.css b/web-ui/styles/modals-core.css index dcb14712..f55dc506 100644 --- a/web-ui/styles/modals-core.css +++ b/web-ui/styles/modals-core.css @@ -725,3 +725,78 @@ background: transparent; box-shadow: none; } + +.prompt-presets-panel { + display: flex; + flex-direction: column; + gap: 14px; + margin-top: 12px; +} + +.prompt-presets-header { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: flex-start; +} + +.prompt-presets-title { + color: var(--color-text-primary); + font-size: var(--font-size-title-sm); + font-weight: var(--font-weight-primary); +} + +.prompt-presets-save-row { + display: grid; + grid-template-columns: minmax(220px, 1fr) auto; + gap: 10px; + align-items: center; +} + +.prompt-presets-list { + display: grid; + gap: 10px; +} + +.prompt-preset-card { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + padding: 12px; + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-card); + background: var(--color-bg-card); +} + +.prompt-preset-main { + min-width: 0; +} + +.prompt-preset-name { + font-weight: var(--font-weight-primary); +} + +.prompt-preset-preview { + max-height: 180px; + margin: 8px 0 0; + padding: 10px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + border-radius: var(--radius-sm); + background: var(--color-bg-subtle); + color: var(--color-text-secondary); + font-family: var(--font-family-mono); + font-size: 11px; +} + +.prompt-preset-actions { + display: flex; + flex-direction: column; + gap: 6px; + align-items: stretch; +} + +.btn-danger-mini { + color: var(--color-danger); +} From 458a1be571e487bfb0b504e8ecd566fdf36d35c9 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 11 Jul 2026 12:58:46 +0000 Subject: [PATCH 03/14] fix(prompts): add preset pool sidebar tab --- tests/unit/config-tabs-ui.test.mjs | 7 +++++++ tests/unit/i18n-locales.test.mjs | 4 +++- web-ui/modules/i18n/locales/en.mjs | 2 ++ web-ui/modules/i18n/locales/ja.mjs | 2 ++ web-ui/modules/i18n/locales/vi.mjs | 2 ++ web-ui/modules/i18n/locales/zh-tw.mjs | 2 ++ web-ui/modules/i18n/locales/zh.mjs | 2 ++ web-ui/partials/index/layout-header.html | 14 ++++++++++++++ web-ui/res/web-ui-render.precompiled.js | 18 ++++++++++++++++++ 9 files changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 3b6294ab..b9c16a1c 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -83,6 +83,13 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(webUiRedirectBlock, /url\.pathname\s*=\s*'\/'/); assert.doesNotMatch(webUiRedirectBlock, /url\.search\s*=/); assert.doesNotMatch(webUiRedirectBlock, /url\.hash\s*=/); + assert.match(html, /id="side-tab-prompts-agents"/); + assert.match(html, /id="side-tab-prompts-project"/); + assert.match(html, /id="side-tab-prompts-presets"/); + assert.match(html, /data-prompts-sub-tab="presets"/); + assert.match(html, /switchPromptsSubTab\('presets'\); onMainTabClick\('prompts'\)/); + assert.match(html, /t\('side\.prompts\.presets'\)/); + assert.match(html, /t\('side\.prompts\.presets\.meta'\)/); assert.match(html, /id="side-tab-orchestration"/); assert.match(html, /id="tab-orchestration"/); assert.match(html, /data-main-tab="orchestration"/); diff --git a/tests/unit/i18n-locales.test.mjs b/tests/unit/i18n-locales.test.mjs index 5b00ba2a..f99033fd 100644 --- a/tests/unit/i18n-locales.test.mjs +++ b/tests/unit/i18n-locales.test.mjs @@ -96,8 +96,10 @@ test('Japanese orchestration template copy stays localized', () => { } }); -test('provider cache and local Web preference settings are localized in every locale', () => { +test('provider cache, prompt sidebar, and local Web preference settings are localized in every locale', () => { const keys = [ + 'side.prompts.presets', + 'side.prompts.presets.meta', 'announcement.providerCache.open', 'announcement.project.eyebrow', 'announcement.project.title', diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 0ef2a52c..f31f0230 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -180,6 +180,8 @@ const en = Object.freeze({ 'side.prompts.agents.meta': 'Codex prompt file', 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude prompt file', + 'side.prompts.presets': 'Preset pool', + 'side.prompts.presets.meta': 'Reusable prompt presets', 'side.plugins.tools': 'Prompt Tools', 'side.plugins.tools.meta': 'Templates / Variables', 'side.plugins.templatesCount': '{count} templates', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 968c8d2d..e3e0311b 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -181,6 +181,8 @@ const ja = Object.freeze({ 'side.prompts.agents.meta': 'Codex プロンプトファイル', 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude プロンプトファイル', + 'side.prompts.presets': 'プリセットプール', + 'side.prompts.presets.meta': '再利用可能なプロンプトプリセット', 'side.plugins.tools': 'プロンプトツール', 'side.plugins.tools.meta': 'テンプレート / 変数', 'side.plugins.templatesCount': '{count} 件のテンプレート', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index 2a22c31e..940ce1e0 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -141,6 +141,8 @@ const vi = Object.freeze({ 'side.prompts.agents.meta': 'Tệp lệnh Codex', 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Tệp lệnh Claude', + 'side.prompts.presets': 'Kho preset', + 'side.prompts.presets.meta': 'Preset prompt tái sử dụng', 'side.plugins.tools': 'Công cụ prompt', 'side.plugins.tools.meta': 'Mẫu / Biến', 'side.plugins.templatesCount': '{count} mẫu', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index f3ef0444..86db7023 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -180,6 +180,8 @@ const zhTw = Object.freeze({ 'side.prompts.agents.meta': 'Codex 指令文件', 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude 指令文件', + 'side.prompts.presets': '預設池', + 'side.prompts.presets.meta': '複用 Prompt 預設', 'side.plugins.tools': '提示詞工具', 'side.plugins.tools.meta': '模板 / 變量', 'side.plugins.templatesCount': '{count} 個模板', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index c3661ec5..8033e4a2 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -180,6 +180,8 @@ const zh = Object.freeze({ 'side.prompts.agents.meta': 'Codex 指令文件', 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude 指令文件', + 'side.prompts.presets': '预设池', + 'side.prompts.presets.meta': '复用 Prompt 预设', 'side.plugins.tools': '提示词工具', 'side.plugins.tools.meta': '模板 / 变量', 'side.plugins.templatesCount': '{count} 个模板', diff --git a/web-ui/partials/index/layout-header.html b/web-ui/partials/index/layout-header.html index efaed430..4f297711 100644 --- a/web-ui/partials/index/layout-header.html +++ b/web-ui/partials/index/layout-header.html @@ -283,6 +283,20 @@ {{ t('side.prompts.claude.meta') }}
+
-
-
-
-
{{ t('prompts.presets.title') }}
-
{{ t('prompts.presets.hint') }}
+
+
+
+ {{ t('prompts.presets.title') }} + {{ t('prompts.presets.hint') }} +
+
+ +
-
- - -
-
{{ t('prompts.presets.empty') }}
-
-
-
- -
{{ t('prompts.presets.updatedAt', { time: formatPromptPresetTime(preset.updatedAt) }) }}
-
{{ preset.content }}
-
-
- - - - +
+
+
{{ t('prompts.presets.empty') }}
+
+
+
+ +
{{ t('prompts.presets.updatedAt', { time: formatPromptPresetTime(preset.updatedAt) }) }}
+
{{ preset.content }}
+
+
+ + + + +
+
-
+
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 6fbe8fce..33dabda9 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -6740,82 +6740,86 @@ return function render(_ctx, _cache) { (_ctx.promptsSubTab === 'presets') ? (_openBlock(), _createElementBlock("div", { key: 1, - class: "prompt-presets-panel" + class: "prompts-editor prompt-presets-panel" }, [ - _createElementVNode("div", { class: "prompt-presets-header" }, [ - _createElementVNode("div", null, [ - _createElementVNode("div", { class: "prompt-presets-title" }, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "form-hint" }, _toDisplayString(_ctx.t('prompts.presets.hint')), 1 /* TEXT */) - ]) - ]), - _createElementVNode("div", { class: "prompt-presets-save-row" }, [ - _withDirectives(_createElementVNode("input", { - type: "text", - class: "form-input", - "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), - placeholder: _ctx.t('prompts.presets.namePlaceholder'), - onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ - [_vModelText, _ctx.promptPresetNameDraft] + _createElementVNode("div", { class: "prompts-editor-toolbar prompt-presets-toolbar" }, [ + _createElementVNode("div", { class: "form-hint" }, [ + _createElementVNode("strong", null, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), + _createElementVNode("span", null, _toDisplayString(_ctx.t('prompts.presets.hint')), 1 /* TEXT */) ]), - _createElementVNode("button", { - type: "button", - class: "btn-primary", - onClick: _ctx.saveCurrentPromptAsPreset, - disabled: _ctx.promptPresetSaving || _ctx.agentsLoading - }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + _createElementVNode("div", { class: "prompts-editor-actions prompt-presets-save-row" }, [ + _withDirectives(_createElementVNode("input", { + type: "text", + class: "form-input prompt-presets-name-input", + "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), + placeholder: _ctx.t('prompts.presets.namePlaceholder'), + onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ + [_vModelText, _ctx.promptPresetNameDraft] + ]), + _createElementVNode("button", { + type: "button", + class: "btn-mini btn-confirm-mini", + onClick: _ctx.saveCurrentPromptAsPreset, + disabled: _ctx.promptPresetSaving || _ctx.agentsLoading + }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + ]) ]), - (!_ctx.promptPresets.length) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "state-message" - }, _toDisplayString(_ctx.t('prompts.presets.empty')), 1 /* TEXT */)) - : (_openBlock(), _createElementBlock("div", { - key: 1, - class: "prompt-presets-list" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { - return (_openBlock(), _createElementBlock("article", { - key: preset.id, - class: "prompt-preset-card" + _createElementVNode("div", { class: "form-group prompt-presets-body" }, [ + _createElementVNode("div", { class: "editor-frame prompt-presets-frame" }, [ + (!_ctx.promptPresets.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "state-message prompt-presets-empty" + }, _toDisplayString(_ctx.t('prompts.presets.empty')), 1 /* TEXT */)) + : (_openBlock(), _createElementBlock("div", { + key: 1, + class: "prompt-presets-list" }, [ - _createElementVNode("div", { class: "prompt-preset-main" }, [ - _createElementVNode("input", { - type: "text", - class: "form-input prompt-preset-name", - value: _ctx.getPromptPresetRenameDraft(preset), - onInput: $event => (_ctx.setPromptPresetRenameDraft(preset.id, $event.target.value)), - onKeydown: _withKeys(_withModifiers($event => (_ctx.renamePromptPreset(preset)), ["prevent"]), ["enter"]), - "aria-label": _ctx.t('prompts.presets.renameAria') - }, null, 40 /* PROPS, NEED_HYDRATION */, ["value", "onInput", "onKeydown", "aria-label"]), - _createElementVNode("div", { class: "form-hint" }, _toDisplayString(_ctx.t('prompts.presets.updatedAt', { time: _ctx.formatPromptPresetTime(preset.updatedAt) })), 1 /* TEXT */), - _createElementVNode("pre", { class: "prompt-preset-preview" }, _toDisplayString(preset.content), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "prompt-preset-actions" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-mini", - onClick: $event => (_ctx.renamePromptPreset(preset)) - }, _toDisplayString(_ctx.t('common.save')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "btn-mini", - onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'codex')) - }, _toDisplayString(_ctx.t('prompts.presets.applyAgents')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "btn-mini", - onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'claude-project')) - }, _toDisplayString(_ctx.t('prompts.presets.applyClaude')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "btn-mini btn-danger-mini", - onClick: $event => (_ctx.deletePromptPreset(preset)) - }, _toDisplayString(_ctx.t('common.delete')), 9 /* TEXT, PROPS */, ["onClick"]) - ]) + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { + return (_openBlock(), _createElementBlock("article", { + key: preset.id, + class: "prompt-preset-row" + }, [ + _createElementVNode("div", { class: "prompt-preset-main" }, [ + _createElementVNode("input", { + type: "text", + class: "form-input prompt-preset-name", + value: _ctx.getPromptPresetRenameDraft(preset), + onInput: $event => (_ctx.setPromptPresetRenameDraft(preset.id, $event.target.value)), + onKeydown: _withKeys(_withModifiers($event => (_ctx.renamePromptPreset(preset)), ["prevent"]), ["enter"]), + "aria-label": _ctx.t('prompts.presets.renameAria') + }, null, 40 /* PROPS, NEED_HYDRATION */, ["value", "onInput", "onKeydown", "aria-label"]), + _createElementVNode("div", { class: "form-hint" }, _toDisplayString(_ctx.t('prompts.presets.updatedAt', { time: _ctx.formatPromptPresetTime(preset.updatedAt) })), 1 /* TEXT */), + _createElementVNode("pre", { class: "prompt-preset-preview" }, _toDisplayString(preset.content), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "prompts-editor-actions prompt-preset-actions" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.renamePromptPreset(preset)) + }, _toDisplayString(_ctx.t('common.save')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'codex')) + }, _toDisplayString(_ctx.t('prompts.presets.applyAgents')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'claude-project')) + }, _toDisplayString(_ctx.t('prompts.presets.applyClaude')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini btn-danger-mini", + onClick: $event => (_ctx.deletePromptPreset(preset)) + }, _toDisplayString(_ctx.t('common.delete')), 9 /* TEXT, PROPS */, ["onClick"]) + ]) + ])) + }), 128 /* KEYED_FRAGMENT */)) ])) - }), 128 /* KEYED_FRAGMENT */)) - ])) + ]) + ]) ])) : (_openBlock(), _createElementBlock("div", { key: 2, diff --git a/web-ui/styles/modals-core.css b/web-ui/styles/modals-core.css index f55dc506..dfa183ef 100644 --- a/web-ui/styles/modals-core.css +++ b/web-ui/styles/modals-core.css @@ -727,45 +727,65 @@ } .prompt-presets-panel { - display: flex; - flex-direction: column; - gap: 14px; margin-top: 12px; } -.prompt-presets-header { +.prompt-presets-toolbar .form-hint { display: flex; - justify-content: space-between; - gap: 12px; - align-items: flex-start; + flex-direction: column; + gap: 2px; + min-width: 0; } -.prompt-presets-title { +.prompt-presets-toolbar .form-hint strong { color: var(--color-text-primary); - font-size: var(--font-size-title-sm); + font-size: var(--font-size-body); font-weight: var(--font-weight-primary); } .prompt-presets-save-row { - display: grid; - grid-template-columns: minmax(220px, 1fr) auto; - gap: 10px; + gap: 8px; +} + +.prompt-presets-name-input { + width: min(340px, 34vw); +} + +.prompt-presets-body { + min-height: 0; +} + +.prompt-presets-frame { + min-height: 75vh; + background: var(--color-bg-card); +} + +.prompt-presets-empty { + margin: 0; + min-height: 75vh; + display: flex; align-items: center; + justify-content: center; + border: none; + background: transparent; } .prompt-presets-list { - display: grid; - gap: 10px; + display: flex; + flex-direction: column; } -.prompt-preset-card { +.prompt-preset-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 12px; - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-card); - background: var(--color-bg-card); + border-bottom: 1px solid var(--color-border-soft); + background: transparent; +} + +.prompt-preset-row:last-child { + border-bottom: none; } .prompt-preset-main { @@ -791,10 +811,10 @@ } .prompt-preset-actions { - display: flex; - flex-direction: column; - gap: 6px; - align-items: stretch; + align-self: start; + flex-wrap: wrap; + justify-content: flex-end; + max-width: 260px; } .btn-danger-mini { From 6b9616494317d17f6228506cc84e02e10229f866 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 11 Jul 2026 14:17:24 +0000 Subject: [PATCH 06/14] fix(prompts): merge sidebar prompt entries --- tests/unit/config-tabs-ui.test.mjs | 14 +++---- tests/unit/i18n-locales.test.mjs | 1 + web-ui/modules/i18n/locales/en.mjs | 1 + web-ui/modules/i18n/locales/ja.mjs | 1 + web-ui/modules/i18n/locales/vi.mjs | 1 + web-ui/modules/i18n/locales/zh-tw.mjs | 1 + web-ui/modules/i18n/locales/zh.mjs | 1 + web-ui/partials/index/layout-header.html | 41 +++----------------- web-ui/res/web-ui-render.precompiled.js | 49 +++--------------------- 9 files changed, 25 insertions(+), 85 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index b5fb6678..9fbb28fd 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -83,13 +83,13 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(webUiRedirectBlock, /url\.pathname\s*=\s*'\/'/); assert.doesNotMatch(webUiRedirectBlock, /url\.search\s*=/); assert.doesNotMatch(webUiRedirectBlock, /url\.hash\s*=/); - assert.match(html, /id="side-tab-prompts-agents"/); - assert.match(html, /id="side-tab-prompts-project"/); - assert.match(html, /id="side-tab-prompts-presets"/); - assert.match(html, /data-prompts-sub-tab="presets"/); - assert.match(html, /switchPromptsSubTab\('presets'\); onMainTabClick\('prompts'\)/); - assert.match(html, /t\('side\.prompts\.presets'\)/); - assert.match(html, /t\('side\.prompts\.presets\.meta'\)/); + assert.match(html, /id="side-tab-prompts"/); + assert.doesNotMatch(html, /id="side-tab-prompts-agents"/); + assert.doesNotMatch(html, /id="side-tab-prompts-project"/); + assert.doesNotMatch(html, /id="side-tab-prompts-presets"/); + assert.match(html, /@click="onMainTabClick\('prompts', \$event\)"/); + assert.match(html, /t\('side\.prompts'\)/); + assert.match(html, /t\('side\.prompts\.meta'\)/); assert.match(html, /v-if="promptsSubTab === 'presets'" class="prompts-editor prompt-presets-panel"/); assert.match(html, /class="prompts-editor-toolbar prompt-presets-toolbar"/); assert.match(html, /class="prompts-editor-actions prompt-presets-save-row"/); diff --git a/tests/unit/i18n-locales.test.mjs b/tests/unit/i18n-locales.test.mjs index f99033fd..d5d616db 100644 --- a/tests/unit/i18n-locales.test.mjs +++ b/tests/unit/i18n-locales.test.mjs @@ -98,6 +98,7 @@ test('Japanese orchestration template copy stays localized', () => { test('provider cache, prompt sidebar, and local Web preference settings are localized in every locale', () => { const keys = [ + 'side.prompts.meta', 'side.prompts.presets', 'side.prompts.presets.meta', 'announcement.providerCache.open', diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index f31f0230..40fcdfc9 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -143,6 +143,7 @@ const en = Object.freeze({ 'side.config': 'Config', 'side.sessions': 'Sessions', 'side.prompts': 'Prompts', + 'side.prompts.meta': 'Prompt files and presets', 'side.plugins': 'Plugins', 'side.system': 'System', 'side.orchestration': 'Tasks', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index e3e0311b..82a3fb4d 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -144,6 +144,7 @@ const ja = Object.freeze({ 'side.config': '設定', 'side.sessions': 'セッション', 'side.prompts': 'Prompts', + 'side.prompts.meta': 'プロンプトファイルとプリセット', 'side.plugins': 'プラグイン', 'side.system': 'システム', 'side.orchestration': 'タスク', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index 940ce1e0..8f2aeae3 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -104,6 +104,7 @@ const vi = Object.freeze({ 'side.config': 'Cấu hình', 'side.sessions': 'Phiên', 'side.prompts': 'Prompts', + 'side.prompts.meta': 'Tệp prompt và preset', 'side.plugins': 'Plugin', 'side.system': 'Hệ thống', 'side.orchestration': 'Tác vụ', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 86db7023..378b68d3 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -143,6 +143,7 @@ const zhTw = Object.freeze({ 'side.config': '設定', 'side.sessions': '會話', 'side.prompts': 'Prompts', + 'side.prompts.meta': 'Prompt 文件與預設', 'side.plugins': '插件', 'side.system': '系統', 'side.orchestration': '任務', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index 8033e4a2..c237b644 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -143,6 +143,7 @@ const zh = Object.freeze({ 'side.config': '配置', 'side.sessions': '会话', 'side.prompts': 'Prompts', + 'side.prompts.meta': 'Prompt 文件与预设', 'side.plugins': '插件', 'side.system': '系统', 'side.orchestration': '任务', diff --git a/web-ui/partials/index/layout-header.html b/web-ui/partials/index/layout-header.html index 4f297711..06d3692a 100644 --- a/web-ui/partials/index/layout-header.html +++ b/web-ui/partials/index/layout-header.html @@ -256,45 +256,16 @@ diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 33dabda9..ecf327ed 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -371,57 +371,20 @@ return function render(_ctx, _cache) { }, [ _createElementVNode("div", { class: "side-section-title" }, _toDisplayString(_ctx.t('side.prompts')), 1 /* TEXT */), _createElementVNode("button", { - id: "side-tab-prompts-agents", + id: "side-tab-prompts", "data-main-tab": "prompts", - "data-prompts-sub-tab": "codex", - "aria-current": _ctx.mainTab === 'prompts' && _ctx.promptsSubTab === 'codex' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isMainTabNavActive('prompts') && _ctx.promptsSubTab === 'codex' }]), + "aria-current": _ctx.mainTab === 'prompts' ? 'page' : null, + class: _normalizeClass(['side-item', { active: _ctx.isMainTabNavActive('prompts') }]), onPointerdown: $event => (_ctx.onMainTabPointerDown('prompts', $event)), - onClick: $event => {_ctx.switchPromptsSubTab('codex'); _ctx.onMainTabClick('prompts')} + onClick: $event => (_ctx.onMainTabClick('prompts', $event)) }, [ _createElementVNode("span", { class: "side-item-icon", "aria-hidden": "true" }, "P"), - _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.prompts.agents')), 1 /* TEXT */), + _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.prompts')), 1 /* TEXT */), _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.prompts.agents.meta')), 1 /* TEXT */) - ]) - ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), - _createElementVNode("button", { - id: "side-tab-prompts-project", - "data-main-tab": "prompts", - "data-prompts-sub-tab": "claude-project", - "aria-current": _ctx.mainTab === 'prompts' && _ctx.promptsSubTab === 'claude-project' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isMainTabNavActive('prompts') && _ctx.promptsSubTab === 'claude-project' }]), - onPointerdown: $event => (_ctx.onMainTabPointerDown('prompts', $event)), - onClick: $event => {_ctx.switchPromptsSubTab('claude-project'); _ctx.onMainTabClick('prompts')} - }, [ - _createElementVNode("span", { - class: "side-item-icon", - "aria-hidden": "true" - }, "M"), - _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.prompts.claude')), 1 /* TEXT */), - _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.prompts.claude.meta')), 1 /* TEXT */) - ]) - ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), - _createElementVNode("button", { - id: "side-tab-prompts-presets", - "data-main-tab": "prompts", - "data-prompts-sub-tab": "presets", - "aria-current": _ctx.mainTab === 'prompts' && _ctx.promptsSubTab === 'presets' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isMainTabNavActive('prompts') && _ctx.promptsSubTab === 'presets' }]), - onPointerdown: $event => (_ctx.onMainTabPointerDown('prompts', $event)), - onClick: $event => {_ctx.switchPromptsSubTab('presets'); _ctx.onMainTabClick('prompts')} - }, [ - _createElementVNode("span", { - class: "side-item-icon", - "aria-hidden": "true" - }, "★"), - _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.prompts.presets')), 1 /* TEXT */), - _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.prompts.presets.meta')), 1 /* TEXT */) + _createElementVNode("span", null, _toDisplayString(_ctx.t('side.prompts.meta')), 1 /* TEXT */) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]) ], 8 /* PROPS */, ["aria-label"]), From 826280765f7df360e45280f14b27e09f18a0116d Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 11 Jul 2026 14:29:08 +0000 Subject: [PATCH 07/14] fix(prompts): remove redundant sidebar prompt section --- tests/unit/config-tabs-ui.test.mjs | 2 ++ web-ui/partials/index/layout-header.html | 6 ++---- web-ui/res/web-ui-render.precompiled.js | 9 +-------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 9fbb28fd..a2964870 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -84,6 +84,8 @@ test('config template keeps expected config tabs in top and side navigation', () assert.doesNotMatch(webUiRedirectBlock, /url\.search\s*=/); assert.doesNotMatch(webUiRedirectBlock, /url\.hash\s*=/); assert.match(html, /id="side-tab-prompts"/); + assert.doesNotMatch(html, /
\{\{ t\('side\.prompts'\) \}\}<\/div>/); + assert.doesNotMatch(html, / -
- - +
+
+ diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 875897e0..5358620c 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -6860,6 +6860,12 @@ return function render(_ctx, _cache) { disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffLoading, title: _ctx.t('prompts.presets.open') }, _toDisplayString(_ctx.t('prompts.subTab.presets')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.saveEditorPromptAsPreset, + disabled: _ctx.promptPresetSaving || _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffLoading || _ctx.agentsDiffVisible || !_ctx.agentsContent.trim(), + title: _ctx.t('prompts.presets.addCurrent') + }, _toDisplayString(_ctx.t('prompts.presets.addCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), _createElementVNode("button", { class: "btn-mini", onClick: _ctx.loadPromptsContent, From e2e40000d2df4df34175315a7e06bdbe1b8b22a2 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 12 Jul 2026 01:42:36 +0000 Subject: [PATCH 09/14] fix(prompts): paste presets into current editor --- tests/unit/config-tabs-ui.test.mjs | 7 ++++--- tests/unit/i18n-locales.test.mjs | 1 + tests/unit/prompt-presets.test.mjs | 14 +++++++------- web-ui/modules/app.methods.agents.mjs | 21 +++------------------ web-ui/modules/i18n/locales/en.mjs | 7 +------ web-ui/modules/i18n/locales/ja.mjs | 7 +------ web-ui/modules/i18n/locales/vi.mjs | 7 +------ web-ui/modules/i18n/locales/zh-tw.mjs | 7 +------ web-ui/modules/i18n/locales/zh.mjs | 7 +------ web-ui/partials/index/panel-prompts.html | 4 +--- web-ui/res/web-ui-render.precompiled.js | 15 ++------------- 11 files changed, 23 insertions(+), 74 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index fa35d9b4..3e642e29 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -95,9 +95,10 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /v-if="promptsSubTab === 'presets'" class="prompts-editor prompt-presets-panel"/); assert.match(html, /class="prompts-editor-toolbar prompt-presets-toolbar"/); assert.match(html, /class="prompts-editor-actions prompt-presets-save-row"/); - assert.match(html, /@click="saveEditorPromptAsPreset"/); - assert.match(html, /t\('prompts\.presets\.addCurrent'\)/); - assert.match(html, /!agentsContent\.trim\(\)/); + assert.match(html, /@click="applyPromptPresetToEditor\(preset\)"/); + assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'codex'\)"/); + assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'claude-project'\)"/); + assert.doesNotMatch(html, /@click="saveEditorPromptAsPreset"/); assert.match(html, /class="form-group prompt-presets-body"/); assert.match(html, /class="editor-frame prompt-presets-frame"/); assert.match(html, /id="side-tab-orchestration"/); diff --git a/tests/unit/i18n-locales.test.mjs b/tests/unit/i18n-locales.test.mjs index 58be6ce8..acd8adc4 100644 --- a/tests/unit/i18n-locales.test.mjs +++ b/tests/unit/i18n-locales.test.mjs @@ -105,6 +105,7 @@ test('provider cache, prompt sidebar, and local Web preference settings are loca 'prompts.presets.defaultName.project', 'prompts.presets.confirm.addCurrentTitle', 'prompts.presets.confirm.addCurrentMessage', + 'prompts.presets.toast.pasted', 'announcement.providerCache.open', 'announcement.project.eyebrow', 'announcement.project.title', diff --git a/tests/unit/prompt-presets.test.mjs b/tests/unit/prompt-presets.test.mjs index 702f4ff8..6ddd8775 100644 --- a/tests/unit/prompt-presets.test.mjs +++ b/tests/unit/prompt-presets.test.mjs @@ -143,23 +143,23 @@ test('prompt presets surface preference persistence failures before success', as assert.notEqual(messages.at(-1)?.message, 'prompts.presets.toast.saved'); }); -test('prompt presets apply to target editor without writing file', async () => { +test('prompt presets paste into current editor draft without target switch or file write', async () => { const { vm, apiCalls, confirms, messages } = createVm({ promptPresets: [{ id: 'p1', name: 'Base', content: 'preset body', updatedAt: '2026-01-01T00:00:00.000Z' }], + promptsSubTab: 'claude-project', agentsOriginalContent: 'original', agentsContent: 'dirty' }); - await vm.applyPromptPresetToEditor(vm.promptPresets[0], 'claude-project'); + await vm.applyPromptPresetToEditor(vm.promptPresets[0]); - assert.equal(confirms.length, 1); + assert.equal(confirms.length, 0); assert.equal(vm.promptsSubTab, 'claude-project'); assert.equal(vm.agentsContent, 'preset body'); - assert.equal(vm.agentsOriginalContent, 'loaded claude'); + assert.equal(vm.agentsOriginalContent, 'original'); assert.equal(vm.selectedPromptPresetId, 'p1'); - assert.equal(messages.at(-1).message, 'prompts.presets.toast.applied'); - assert.deepEqual(apiCalls.map(call => call.action), ['get-claude-md-file']); - assert.ok(!apiCalls.some(call => call.action.startsWith('apply-'))); + assert.equal(messages.at(-1).message, 'prompts.presets.toast.pasted'); + assert.deepEqual(apiCalls, []); }); test('prompt presets prevent rename conflicts and delete after confirmation', async () => { diff --git a/web-ui/modules/app.methods.agents.mjs b/web-ui/modules/app.methods.agents.mjs index d426de2c..57e39776 100644 --- a/web-ui/modules/app.methods.agents.mjs +++ b/web-ui/modules/app.methods.agents.mjs @@ -831,27 +831,12 @@ export function createAgentsMethods(options = {}) { this.promptPresetSaving = false; } }, - async applyPromptPresetToEditor(preset, target) { - if (!preset || typeof preset.content !== 'string') return; - const normalizedTarget = target === 'claude-project' ? 'claude-project' : 'codex'; - if (this.hasAgentsContentChanged()) { - const confirmed = await this.requestConfirmDialog({ - title: this.t('prompts.presets.confirm.applyTitle'), - message: this.t('prompts.presets.confirm.applyMessage'), - confirmText: this.t('prompts.presets.confirm.applyConfirm'), - cancelText: this.t('common.cancel'), - danger: true - }); - if (!confirmed) return; - } - this.__skipNextPromptsSubTabLoad = true; - this.promptsSubTab = normalizedTarget; - await this.$nextTick(); - await this.loadPromptsContent(); + async applyPromptPresetToEditor(preset) { + if (!preset || typeof preset.content !== 'string' || !preset.content) return; this.agentsContent = preset.content; this.onAgentsContentInput(); this.selectedPromptPresetId = preset.id; - this.showMessage(this.t('prompts.presets.toast.applied'), 'success'); + this.showMessage(this.t('prompts.presets.toast.pasted'), 'success'); }, async renamePromptPreset(preset) { if (!preset || !preset.id) return; diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 6e336137..2a99caf0 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -234,8 +234,6 @@ const en = Object.freeze({ 'prompts.presets.empty': 'No presets yet. Open AGENTS.md or CLAUDE.md, enter content, then save it as a preset.', 'prompts.presets.updatedAt': 'Updated: {time}', 'prompts.presets.renameAria': 'Rename prompt preset', - 'prompts.presets.applyAgents': 'Apply to AGENTS.md', - 'prompts.presets.applyClaude': 'Apply to CLAUDE.md', 'prompts.presets.open': 'Open preset pool', 'prompts.presets.addCurrent': 'Add to preset pool', 'prompts.presets.defaultName.project': 'Project CLAUDE.md - {path}', @@ -247,13 +245,10 @@ const en = Object.freeze({ 'prompts.presets.confirm.overwriteTitle': 'Overwrite preset?', 'prompts.presets.confirm.overwriteMessage': 'A preset named “{name}” already exists. Overwrite it with the current editor content?', 'prompts.presets.confirm.overwriteConfirm': 'Overwrite', - 'prompts.presets.confirm.applyTitle': 'Replace current editor content?', - 'prompts.presets.confirm.applyMessage': 'The current editor has unsaved changes. Applying this preset will replace the editor content, but it will not write the file until you click Save.', - 'prompts.presets.confirm.applyConfirm': 'Apply preset', 'prompts.presets.confirm.deleteTitle': 'Delete preset?', 'prompts.presets.confirm.deleteMessage': 'Delete preset “{name}”? This cannot be undone.', 'prompts.presets.toast.saved': 'Preset saved', - 'prompts.presets.toast.applied': 'Preset applied to editor. Click Save to write the file.', + 'prompts.presets.toast.pasted': 'Current editor content replaced. File not saved yet.', 'prompts.presets.toast.renamed': 'Preset renamed', 'prompts.presets.toast.deleted': 'Preset deleted', 'prompts.project.pathLabel': 'Project path (optional)', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 627712a2..56c5362f 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -235,8 +235,6 @@ const ja = Object.freeze({ 'prompts.presets.empty': 'プリセットはまだありません。AGENTS.md または CLAUDE.md を開いて内容を編集し、プリセットとして保存してください。', 'prompts.presets.updatedAt': '更新: {time}', 'prompts.presets.renameAria': 'prompt プリセット名を変更', - 'prompts.presets.applyAgents': 'AGENTS.md に適用', - 'prompts.presets.applyClaude': 'CLAUDE.md に適用', 'prompts.presets.open': 'プリセットを開く', 'prompts.presets.addCurrent': 'プリセットプールに追加', 'prompts.presets.defaultName.project': 'プロジェクト CLAUDE.md - {path}', @@ -248,13 +246,10 @@ const ja = Object.freeze({ 'prompts.presets.confirm.overwriteTitle': 'プリセットを上書きしますか?', 'prompts.presets.confirm.overwriteMessage': '「{name}」というプリセットは既に存在します。現在のエディター内容で上書きしますか?', 'prompts.presets.confirm.overwriteConfirm': '上書き', - 'prompts.presets.confirm.applyTitle': '現在のエディター内容を置き換えますか?', - 'prompts.presets.confirm.applyMessage': '現在のエディターには未保存の変更があります。プリセットを適用するとエディター内容は置き換わりますが、保存をクリックするまでファイルには書き込まれません。', - 'prompts.presets.confirm.applyConfirm': 'プリセットを適用', 'prompts.presets.confirm.deleteTitle': 'プリセットを削除しますか?', 'prompts.presets.confirm.deleteMessage': 'プリセット「{name}」を削除しますか?この操作は元に戻せません。', 'prompts.presets.toast.saved': 'プリセットを保存しました', - 'prompts.presets.toast.applied': 'プリセットをエディターへ適用しました。ファイルへ書き込むには保存してください。', + 'prompts.presets.toast.pasted': '現在のエディター内容を置き換えました。ファイルはまだ保存されていません。', 'prompts.presets.toast.renamed': 'プリセット名を変更しました', 'prompts.presets.toast.deleted': 'プリセットを削除しました', 'prompts.project.pathLabel': 'プロジェクトパス(任意)', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index 3347f9a6..e74ccb68 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -249,8 +249,6 @@ const vi = Object.freeze({ 'prompts.presets.empty': 'Chưa có preset. Mở AGENTS.md hoặc CLAUDE.md, nhập nội dung rồi lưu thành preset.', 'prompts.presets.updatedAt': 'Cập nhật: {time}', 'prompts.presets.renameAria': 'Đổi tên preset prompt', - 'prompts.presets.applyAgents': 'Áp dụng vào AGENTS.md', - 'prompts.presets.applyClaude': 'Áp dụng vào CLAUDE.md', 'prompts.presets.open': 'Mở kho preset', 'prompts.presets.addCurrent': 'Thêm vào kho preset', 'prompts.presets.defaultName.project': 'CLAUDE.md dự án - {path}', @@ -262,13 +260,10 @@ const vi = Object.freeze({ 'prompts.presets.confirm.overwriteTitle': 'Ghi đè preset?', 'prompts.presets.confirm.overwriteMessage': 'Preset “{name}” đã tồn tại. Ghi đè bằng nội dung editor hiện tại?', 'prompts.presets.confirm.overwriteConfirm': 'Ghi đè', - 'prompts.presets.confirm.applyTitle': 'Thay nội dung editor hiện tại?', - 'prompts.presets.confirm.applyMessage': 'Editor hiện có thay đổi chưa lưu. Áp dụng preset sẽ thay nội dung editor, nhưng chưa ghi tệp cho tới khi bạn bấm Lưu.', - 'prompts.presets.confirm.applyConfirm': 'Áp dụng preset', 'prompts.presets.confirm.deleteTitle': 'Xóa preset?', 'prompts.presets.confirm.deleteMessage': 'Xóa preset “{name}”? Không thể hoàn tác.', 'prompts.presets.toast.saved': 'Đã lưu preset', - 'prompts.presets.toast.applied': 'Đã áp dụng preset vào editor. Bấm Lưu để ghi tệp.', + 'prompts.presets.toast.pasted': 'Đã thay nội dung trình soạn thảo hiện tại. Tệp chưa được lưu.', 'prompts.presets.toast.renamed': 'Đã đổi tên preset', 'prompts.presets.toast.deleted': 'Đã xóa preset', 'prompts.project.pathLabel': 'Đường dẫn dự án (tùy chọn)', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 1b00e6f7..0f752591 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -234,8 +234,6 @@ const zhTw = Object.freeze({ 'prompts.presets.empty': '暫無預設。先打開 AGENTS.md 或 CLAUDE.md 編輯內容,再保存為預設。', 'prompts.presets.updatedAt': '更新時間:{time}', 'prompts.presets.renameAria': '重新命名 prompt 預設', - 'prompts.presets.applyAgents': '套用到 AGENTS.md', - 'prompts.presets.applyClaude': '套用到 CLAUDE.md', 'prompts.presets.open': '打開預設池', 'prompts.presets.addCurrent': '加入預設池', 'prompts.presets.defaultName.project': '專案 CLAUDE.md - {path}', @@ -247,13 +245,10 @@ const zhTw = Object.freeze({ 'prompts.presets.confirm.overwriteTitle': '覆蓋預設?', 'prompts.presets.confirm.overwriteMessage': '名為「{name}」的預設已存在。要用目前編輯器內容覆蓋它嗎?', 'prompts.presets.confirm.overwriteConfirm': '覆蓋', - 'prompts.presets.confirm.applyTitle': '替換目前編輯器內容?', - 'prompts.presets.confirm.applyMessage': '目前編輯器存在未保存改動。套用預設會替換編輯器內容,但不會寫入文件,仍需點擊保存。', - 'prompts.presets.confirm.applyConfirm': '套用預設', 'prompts.presets.confirm.deleteTitle': '刪除預設?', 'prompts.presets.confirm.deleteMessage': '刪除預設「{name}」?此操作不可復原。', 'prompts.presets.toast.saved': '預設已保存', - 'prompts.presets.toast.applied': '預設已套用到編輯器,點擊保存後才會寫入文件。', + 'prompts.presets.toast.pasted': '已替換目前編輯區內容,尚未保存文件。', 'prompts.presets.toast.renamed': '預設已重新命名', 'prompts.presets.toast.deleted': '預設已刪除', 'prompts.project.pathLabel': '專案路徑(可選)', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index ccbcb657..db9f0984 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -234,8 +234,6 @@ const zh = Object.freeze({ 'prompts.presets.empty': '暂无预设。先打开 AGENTS.md 或 CLAUDE.md 编辑内容,再保存为预设。', 'prompts.presets.updatedAt': '更新时间:{time}', 'prompts.presets.renameAria': '重命名 prompt 预设', - 'prompts.presets.applyAgents': '应用到 AGENTS.md', - 'prompts.presets.applyClaude': '应用到 CLAUDE.md', 'prompts.presets.open': '打开预设池', 'prompts.presets.addCurrent': '添加到预设池', 'prompts.presets.defaultName.project': '项目 CLAUDE.md - {path}', @@ -247,13 +245,10 @@ const zh = Object.freeze({ 'prompts.presets.confirm.overwriteTitle': '覆盖预设?', 'prompts.presets.confirm.overwriteMessage': '名为“{name}”的预设已存在。要用当前编辑器内容覆盖它吗?', 'prompts.presets.confirm.overwriteConfirm': '覆盖', - 'prompts.presets.confirm.applyTitle': '替换当前编辑器内容?', - 'prompts.presets.confirm.applyMessage': '当前编辑器存在未保存改动。应用预设会替换编辑器内容,但不会写入文件,仍需点击保存。', - 'prompts.presets.confirm.applyConfirm': '应用预设', 'prompts.presets.confirm.deleteTitle': '删除预设?', 'prompts.presets.confirm.deleteMessage': '删除预设“{name}”?此操作不可撤销。', 'prompts.presets.toast.saved': '预设已保存', - 'prompts.presets.toast.applied': '预设已应用到编辑器,点击保存后才会写入文件。', + 'prompts.presets.toast.pasted': '已替换当前编辑区内容,尚未保存文件。', 'prompts.presets.toast.renamed': '预设已重命名', 'prompts.presets.toast.deleted': '预设已删除', 'prompts.project.pathLabel': '项目路径(可选)', diff --git a/web-ui/partials/index/panel-prompts.html b/web-ui/partials/index/panel-prompts.html index 9f7cad89..4b112e3f 100644 --- a/web-ui/partials/index/panel-prompts.html +++ b/web-ui/partials/index/panel-prompts.html @@ -73,8 +73,7 @@
- - +
@@ -117,7 +116,6 @@
- diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 5358620c..2db02b07 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -6758,13 +6758,8 @@ return function render(_ctx, _cache) { _createElementVNode("button", { type: "button", class: "btn-mini", - onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'codex')) - }, _toDisplayString(_ctx.t('prompts.presets.applyAgents')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "btn-mini", - onClick: $event => (_ctx.applyPromptPresetToEditor(preset, 'claude-project')) - }, _toDisplayString(_ctx.t('prompts.presets.applyClaude')), 9 /* TEXT, PROPS */, ["onClick"]), + onClick: $event => (_ctx.applyPromptPresetToEditor(preset)) + }, _toDisplayString(_ctx.t('common.paste')), 9 /* TEXT, PROPS */, ["onClick"]), _createElementVNode("button", { type: "button", class: "btn-mini btn-danger-mini", @@ -6860,12 +6855,6 @@ return function render(_ctx, _cache) { disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffLoading, title: _ctx.t('prompts.presets.open') }, _toDisplayString(_ctx.t('prompts.subTab.presets')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), - _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.saveEditorPromptAsPreset, - disabled: _ctx.promptPresetSaving || _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffLoading || _ctx.agentsDiffVisible || !_ctx.agentsContent.trim(), - title: _ctx.t('prompts.presets.addCurrent') - }, _toDisplayString(_ctx.t('prompts.presets.addCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), _createElementVNode("button", { class: "btn-mini", onClick: _ctx.loadPromptsContent, From 01539835ffa9420980e7e20ee8fff70492fd651a Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 12 Jul 2026 02:53:35 +0000 Subject: [PATCH 10/14] fix(prompts): keep presets in current editor --- tests/unit/config-tabs-ui.test.mjs | 7 +- tests/unit/i18n-locales.test.mjs | 1 + tests/unit/prompt-presets.test.mjs | 21 + tests/unit/web-ui-behavior-parity.test.mjs | 1 + web-ui/app.js | 6 +- web-ui/modules/app.methods.agents.mjs | 17 +- .../app.methods.web-ui-preferences.mjs | 2 +- web-ui/modules/i18n/locales/en.mjs | 1 + web-ui/modules/i18n/locales/ja.mjs | 1 + web-ui/modules/i18n/locales/vi.mjs | 1 + web-ui/modules/i18n/locales/zh-tw.mjs | 1 + web-ui/modules/i18n/locales/zh.mjs | 1 + web-ui/partials/index/panel-prompts.html | 109 ++-- web-ui/res/web-ui-render.precompiled.js | 574 +++++++++--------- web-ui/styles/modals-core.css | 35 +- 15 files changed, 412 insertions(+), 366 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 3e642e29..213ff101 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -92,7 +92,12 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /@click="onMainTabClick\('prompts', \$event\)"/); assert.match(html, /t\('side\.prompts'\)/); assert.match(html, /t\('side\.prompts\.meta'\)/); - assert.match(html, /v-if="promptsSubTab === 'presets'" class="prompts-editor prompt-presets-panel"/); + assert.doesNotMatch(html, /promptsSubTab === 'presets'/); + assert.doesNotMatch(html, /switchPromptsSubTab\('presets'\)/); + assert.match(html, /
/); + assert.match(html, /class="form-input prompt-presets-select"/); + assert.match(html, /@change="applyPromptPresetSelection\(\$event\)"/); + assert.match(html, /t\('prompts\.presets\.selectPlaceholder'\)/); assert.match(html, /class="prompts-editor-toolbar prompt-presets-toolbar"/); assert.match(html, /class="prompts-editor-actions prompt-presets-save-row"/); assert.match(html, /@click="applyPromptPresetToEditor\(preset\)"/); diff --git a/tests/unit/i18n-locales.test.mjs b/tests/unit/i18n-locales.test.mjs index acd8adc4..1bfc946e 100644 --- a/tests/unit/i18n-locales.test.mjs +++ b/tests/unit/i18n-locales.test.mjs @@ -106,6 +106,7 @@ test('provider cache, prompt sidebar, and local Web preference settings are loca 'prompts.presets.confirm.addCurrentTitle', 'prompts.presets.confirm.addCurrentMessage', 'prompts.presets.toast.pasted', + 'prompts.presets.selectPlaceholder', 'announcement.providerCache.open', 'announcement.project.eyebrow', 'announcement.project.title', diff --git a/tests/unit/prompt-presets.test.mjs b/tests/unit/prompt-presets.test.mjs index 6ddd8775..15358637 100644 --- a/tests/unit/prompt-presets.test.mjs +++ b/tests/unit/prompt-presets.test.mjs @@ -162,6 +162,27 @@ test('prompt presets paste into current editor draft without target switch or fi assert.deepEqual(apiCalls, []); }); +test('prompt preset selection pastes into current editor and resets selector', async () => { + const { vm, apiCalls, confirms, messages } = createVm({ + promptPresets: [{ id: 'p1', name: 'Base', content: 'selected body', updatedAt: '2026-01-01T00:00:00.000Z' }], + promptsSubTab: 'codex', + agentsOriginalContent: 'original', + agentsContent: 'dirty' + }); + const event = { target: { value: 'p1' } }; + + await vm.applyPromptPresetSelection(event); + + assert.equal(confirms.length, 0); + assert.equal(vm.promptsSubTab, 'codex'); + assert.equal(vm.agentsContent, 'selected body'); + assert.equal(vm.agentsOriginalContent, 'original'); + assert.equal(vm.selectedPromptPresetId, 'p1'); + assert.equal(event.target.value, ''); + assert.equal(messages.at(-1).message, 'prompts.presets.toast.pasted'); + assert.deepEqual(apiCalls, []); +}); + test('prompt presets prevent rename conflicts and delete after confirmation', async () => { const { vm, persisted, messages, confirms } = createVm({ promptPresets: [ diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 3d56e2bb..2acb042e 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -656,6 +656,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'appendTaskWorkflowId', 'openClaudeMdEditor', 'applyPromptPresetToEditor', + 'applyPromptPresetSelection', 'buildPromptPresetId', 'deletePromptPreset', 'findPromptPresetByName', diff --git a/web-ui/app.js b/web-ui/app.js index 243e4d64..48b8c3fd 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -736,9 +736,7 @@ document.addEventListener('DOMContentLoaded', () => { if (this.promptsSubTab === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading && typeof this.loadProjectPathOptions === 'function') { this.loadProjectPathOptions(); } - if (this.promptsSubTab !== 'presets') { - this.loadPromptsContent(); - } + this.loadPromptsContent(); } }, promptsSubTab(newVal) { @@ -749,7 +747,7 @@ document.addEventListener('DOMContentLoaded', () => { this.__skipNextPromptsSubTabLoad = false; return; } - if (this.mainTab === 'prompts' && newVal !== 'presets' && typeof this.loadPromptsContent === 'function') { + if (this.mainTab === 'prompts' && typeof this.loadPromptsContent === 'function') { this.loadPromptsContent(); } }, diff --git a/web-ui/modules/app.methods.agents.mjs b/web-ui/modules/app.methods.agents.mjs index 57e39776..bf13c266 100644 --- a/web-ui/modules/app.methods.agents.mjs +++ b/web-ui/modules/app.methods.agents.mjs @@ -838,6 +838,14 @@ export function createAgentsMethods(options = {}) { this.selectedPromptPresetId = preset.id; this.showMessage(this.t('prompts.presets.toast.pasted'), 'success'); }, + async applyPromptPresetSelection(event) { + const select = event && event.target; + const presetId = select && typeof select.value === 'string' ? select.value : ''; + if (!presetId) return; + const preset = (Array.isArray(this.promptPresets) ? this.promptPresets : []).find((item) => item && item.id === presetId); + await this.applyPromptPresetToEditor(preset); + if (select) select.value = ''; + }, async renamePromptPreset(preset) { if (!preset || !preset.id) return; const name = this.normalizePromptPresetName(this.getPromptPresetRenameDraft(preset)); @@ -885,14 +893,7 @@ export function createAgentsMethods(options = {}) { this.showMessage(this.t('prompts.presets.toast.deleted'), 'success'); }, switchPromptsSubTab(subTab) { - const normalized = subTab === 'claude-project' || subTab === 'presets' ? subTab : 'codex'; - if (normalized === 'presets') { - this.promptsSubTab = normalized; - this.$nextTick(() => { - document.querySelector('.main-panel')?.scrollTo({ top: 0, left: 0, behavior: 'auto' }); - }); - return; - } + const normalized = subTab === 'claude-project' ? subTab : 'codex'; if (normalized === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading) { this.loadProjectPathOptions(); } diff --git a/web-ui/modules/app.methods.web-ui-preferences.mjs b/web-ui/modules/app.methods.web-ui-preferences.mjs index 16600244..dc22bd1b 100644 --- a/web-ui/modules/app.methods.web-ui-preferences.mjs +++ b/web-ui/modules/app.methods.web-ui-preferences.mjs @@ -62,7 +62,7 @@ function normalizeUsageTimeRange(value) { function normalizePromptsSubTab(value) { const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''; - if (normalized === 'claude-project' || normalized === 'presets') return normalized; + if (normalized === 'claude-project') return normalized; return 'codex'; } diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 2a99caf0..b1f13e66 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -230,6 +230,7 @@ const en = Object.freeze({ 'prompts.presets.title': 'Prompt preset pool', 'prompts.presets.hint': 'Save reusable prompt content here, then apply it to AGENTS.md or CLAUDE.md. Applying a preset only updates the editor; click Save to write the file.', 'prompts.presets.namePlaceholder': 'Preset name', + 'prompts.presets.selectPlaceholder': 'Paste preset…', 'prompts.presets.saveCurrent': 'Save current as preset', 'prompts.presets.empty': 'No presets yet. Open AGENTS.md or CLAUDE.md, enter content, then save it as a preset.', 'prompts.presets.updatedAt': 'Updated: {time}', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 56c5362f..d53107d0 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -231,6 +231,7 @@ const ja = Object.freeze({ 'prompts.presets.title': 'Prompt 共通プリセット', 'prompts.presets.hint': '再利用する prompt 内容を保存し、AGENTS.md または CLAUDE.md に適用できます。適用してもエディターだけが更新され、ファイルへ書き込むには保存が必要です。', 'prompts.presets.namePlaceholder': 'プリセット名', + 'prompts.presets.selectPlaceholder': 'プリセットを選んでペースト…', 'prompts.presets.saveCurrent': '現在の内容をプリセット保存', 'prompts.presets.empty': 'プリセットはまだありません。AGENTS.md または CLAUDE.md を開いて内容を編集し、プリセットとして保存してください。', 'prompts.presets.updatedAt': '更新: {time}', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index e74ccb68..f7b167f0 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -245,6 +245,7 @@ const vi = Object.freeze({ 'prompts.presets.title': 'Kho preset prompt dùng chung', 'prompts.presets.hint': 'Lưu nội dung prompt tái sử dụng tại đây, rồi áp dụng vào AGENTS.md hoặc CLAUDE.md. Áp dụng preset chỉ cập nhật editor; cần bấm Lưu để ghi tệp.', 'prompts.presets.namePlaceholder': 'Tên preset', + 'prompts.presets.selectPlaceholder': 'Chọn preset để dán…', 'prompts.presets.saveCurrent': 'Lưu nội dung hiện tại thành preset', 'prompts.presets.empty': 'Chưa có preset. Mở AGENTS.md hoặc CLAUDE.md, nhập nội dung rồi lưu thành preset.', 'prompts.presets.updatedAt': 'Cập nhật: {time}', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 0f752591..d4d187cb 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -230,6 +230,7 @@ const zhTw = Object.freeze({ 'prompts.presets.title': 'Prompt 通用預設池', 'prompts.presets.hint': '把可重用的 prompt 內容保存在這裡,再套用到 AGENTS.md 或 CLAUDE.md。套用預設只更新編輯器,仍需點擊保存才會寫入文件。', 'prompts.presets.namePlaceholder': '預設名稱', + 'prompts.presets.selectPlaceholder': '選擇預設並貼上…', 'prompts.presets.saveCurrent': '保存目前內容為預設', 'prompts.presets.empty': '暫無預設。先打開 AGENTS.md 或 CLAUDE.md 編輯內容,再保存為預設。', 'prompts.presets.updatedAt': '更新時間:{time}', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index db9f0984..1ca19314 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -230,6 +230,7 @@ const zh = Object.freeze({ 'prompts.presets.title': 'Prompt 通用预设池', 'prompts.presets.hint': '把可复用的 prompt 内容保存在这里,再应用到 AGENTS.md 或 CLAUDE.md。应用预设只更新编辑器,仍需点击保存才会写入文件。', 'prompts.presets.namePlaceholder': '预设名称', + 'prompts.presets.selectPlaceholder': '选择预设并粘贴…', 'prompts.presets.saveCurrent': '保存当前内容为预设', 'prompts.presets.empty': '暂无预设。先打开 AGENTS.md 或 CLAUDE.md 编辑内容,再保存为预设。', 'prompts.presets.updatedAt': '更新时间:{time}', diff --git a/web-ui/partials/index/panel-prompts.html b/web-ui/partials/index/panel-prompts.html index 4b112e3f..d8ca25cd 100644 --- a/web-ui/partials/index/panel-prompts.html +++ b/web-ui/partials/index/panel-prompts.html @@ -8,7 +8,6 @@
-
@@ -33,56 +32,8 @@
-
-
-
- {{ t('prompts.presets.title') }} - {{ t('prompts.presets.hint') }} -
-
- - -
-
-
-
-
{{ t('prompts.presets.empty') }}
-
-
-
- -
{{ t('prompts.presets.updatedAt', { time: formatPromptPresetTime(preset.updatedAt) }) }}
-
{{ preset.content }}
-
-
- - - -
-
-
-
-
-
-
+
{{ agentsPath || t('common.notLoaded') }} @@ -115,7 +66,14 @@
- + @@ -134,6 +92,55 @@
+
+ + {{ t('prompts.presets.title') }} + {{ t('prompts.presets.hint') }} + +
+
+ + +
+
+
+
+
{{ t('prompts.presets.empty') }}
+
+
+
+ +
{{ t('prompts.presets.updatedAt', { time: formatPromptPresetTime(preset.updatedAt) }) }}
+
{{ preset.content }}
+
+
+ + + +
+
+
+
+
+
+
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 2db02b07..c83375fd 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -6648,14 +6648,7 @@ return function render(_ctx, _cache) { role: "tab", "aria-selected": _ctx.promptsSubTab === 'claude-project', onClick: $event => (_ctx.switchPromptsSubTab('claude-project')) - }, _toDisplayString(_ctx.t('prompts.subTab.project')), 11 /* TEXT, CLASS, PROPS */, ["aria-selected", "onClick"]), - _createElementVNode("button", { - type: "button", - class: _normalizeClass(['prompts-md-tab', { active: _ctx.promptsSubTab === 'presets' }]), - role: "tab", - "aria-selected": _ctx.promptsSubTab === 'presets', - onClick: $event => (_ctx.switchPromptsSubTab('presets')) - }, _toDisplayString(_ctx.t('prompts.subTab.presets')), 11 /* TEXT, CLASS, PROPS */, ["aria-selected", "onClick"]) + }, _toDisplayString(_ctx.t('prompts.subTab.project')), 11 /* TEXT, CLASS, PROPS */, ["aria-selected", "onClick"]) ], 8 /* PROPS */, ["aria-label"]), (_ctx.promptsSubTab === 'claude-project') ? (_openBlock(), _createElementBlock("div", { @@ -6693,103 +6686,117 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true) ])) : _createCommentVNode("v-if", true), - (_ctx.promptsSubTab === 'presets') - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "prompts-editor prompt-presets-panel" - }, [ - _createElementVNode("div", { class: "prompts-editor-toolbar prompt-presets-toolbar" }, [ - _createElementVNode("div", { class: "form-hint" }, [ - _createElementVNode("strong", null, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), - _createElementVNode("span", null, _toDisplayString(_ctx.t('prompts.presets.hint')), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "prompts-editor-actions prompt-presets-save-row" }, [ - _withDirectives(_createElementVNode("input", { - type: "text", - class: "form-input prompt-presets-name-input", - "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), - placeholder: _ctx.t('prompts.presets.namePlaceholder'), - onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ - [_vModelText, _ctx.promptPresetNameDraft] - ]), - _createElementVNode("button", { - type: "button", - class: "btn-mini btn-confirm-mini", - onClick: _ctx.saveCurrentPromptAsPreset, - disabled: _ctx.promptPresetSaving || _ctx.agentsLoading - }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) - ]) + _createElementVNode("div", { class: "prompts-editor" }, [ + _createElementVNode("div", { class: "prompts-editor-toolbar" }, [ + _createElementVNode("div", { class: "form-hint" }, [ + _createTextVNode(_toDisplayString(_ctx.agentsPath || _ctx.t('common.notLoaded')) + " ", 1 /* TEXT */), + (_ctx.agentsPath) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, " (" + _toDisplayString(_ctx.agentsExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave')) + ") ", 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]), + _createElementVNode("div", { class: "prompts-editor-actions" }, [ + _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--secondary" }, [ + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.exportAgentsContent, + disabled: _ctx.agentsLoading, + title: _ctx.t('modal.agents.export') + }, [ + (_openBlock(), _createElementBlock("svg", { + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" + }, [ + _createElementVNode("path", { d: "M12 3v12M8 11l4 4 4-4M4 17v2h16v-2" }) + ])) + ], 8 /* PROPS */, ["onClick", "disabled", "title"]), + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.copyAgentsContent, + disabled: _ctx.agentsLoading, + title: _ctx.t('modal.agents.copy') + }, [ + (_openBlock(), _createElementBlock("svg", { + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" + }, [ + _createElementVNode("rect", { + x: "9", + y: "9", + width: "13", + height: "13", + rx: "2" + }), + _createElementVNode("path", { d: "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" }) + ])) + ], 8 /* PROPS */, ["onClick", "disabled", "title"]), + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.pasteAgentsContent, + disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + title: _ctx.t('common.paste') + }, [ + (_openBlock(), _createElementBlock("svg", { + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" + }, [ + _createElementVNode("rect", { + x: "8", + y: "2", + width: "8", + height: "4", + rx: "1" + }), + _createElementVNode("path", { d: "M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" }) + ])) + ], 8 /* PROPS */, ["onClick", "disabled", "title"]) ]), - _createElementVNode("div", { class: "form-group prompt-presets-body" }, [ - _createElementVNode("div", { class: "editor-frame prompt-presets-frame" }, [ - (!_ctx.promptPresets.length) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "state-message prompt-presets-empty" - }, _toDisplayString(_ctx.t('prompts.presets.empty')), 1 /* TEXT */)) - : (_openBlock(), _createElementBlock("div", { - key: 1, - class: "prompt-presets-list" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { - return (_openBlock(), _createElementBlock("article", { - key: preset.id, - class: "prompt-preset-row" - }, [ - _createElementVNode("div", { class: "prompt-preset-main" }, [ - _createElementVNode("input", { - type: "text", - class: "form-input prompt-preset-name", - value: _ctx.getPromptPresetRenameDraft(preset), - onInput: $event => (_ctx.setPromptPresetRenameDraft(preset.id, $event.target.value)), - onKeydown: _withKeys(_withModifiers($event => (_ctx.renamePromptPreset(preset)), ["prevent"]), ["enter"]), - "aria-label": _ctx.t('prompts.presets.renameAria') - }, null, 40 /* PROPS, NEED_HYDRATION */, ["value", "onInput", "onKeydown", "aria-label"]), - _createElementVNode("div", { class: "form-hint" }, _toDisplayString(_ctx.t('prompts.presets.updatedAt', { time: _ctx.formatPromptPresetTime(preset.updatedAt) })), 1 /* TEXT */), - _createElementVNode("pre", { class: "prompt-preset-preview" }, _toDisplayString(preset.content), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "prompts-editor-actions prompt-preset-actions" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-mini", - onClick: $event => (_ctx.renamePromptPreset(preset)) - }, _toDisplayString(_ctx.t('common.save')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "btn-mini", - onClick: $event => (_ctx.applyPromptPresetToEditor(preset)) - }, _toDisplayString(_ctx.t('common.paste')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "btn-mini btn-danger-mini", - onClick: $event => (_ctx.deletePromptPreset(preset)) - }, _toDisplayString(_ctx.t('common.delete')), 9 /* TEXT, PROPS */, ["onClick"]) - ]) - ])) - }), 128 /* KEYED_FRAGMENT */)) - ])) - ]) - ]) - ])) - : (_openBlock(), _createElementBlock("div", { - key: 2, - class: "prompts-editor" - }, [ - _createElementVNode("div", { class: "prompts-editor-toolbar" }, [ - _createElementVNode("div", { class: "form-hint" }, [ - _createTextVNode(_toDisplayString(_ctx.agentsPath || _ctx.t('common.notLoaded')) + " ", 1 /* TEXT */), - (_ctx.agentsPath) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, " (" + _toDisplayString(_ctx.agentsExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave')) + ") ", 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]), - _createElementVNode("div", { class: "prompts-editor-actions" }, [ - _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--secondary" }, [ - _createElementVNode("button", { + _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--workflow" }, [ + _createElementVNode("select", { + class: "form-input prompt-presets-select", + disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible || !_ctx.promptPresets.length, + title: _ctx.t('prompts.presets.title'), + onChange: $event => (_ctx.applyPromptPresetSelection($event)) + }, [ + _createElementVNode("option", { value: "" }, _toDisplayString(_ctx.t('prompts.presets.selectPlaceholder')), 1 /* TEXT */), + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { + return (_openBlock(), _createElementBlock("option", { + key: preset.id, + value: preset.id + }, _toDisplayString(preset.name), 9 /* TEXT, PROPS */, ["value"])) + }), 128 /* KEYED_FRAGMENT */)) + ], 40 /* PROPS, NEED_HYDRATION */, ["disabled", "title", "onChange"]), + _createElementVNode("button", { + class: "btn-mini", + onClick: _ctx.loadPromptsContent, + disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, + title: _ctx.t('common.cancel') + }, [ + (_openBlock(), _createElementBlock("svg", { + class: "btn-icon-sm", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2" + }, [ + _createElementVNode("path", { d: "M18 6L6 18M6 6l12 12" }) + ])) + ], 8 /* PROPS */, ["onClick", "disabled", "title"]), + (_ctx.agentsDiffVisible) + ? (_openBlock(), _createElementBlock("button", { + key: 0, class: "btn-mini", - onClick: _ctx.exportAgentsContent, - disabled: _ctx.agentsLoading, - title: _ctx.t('modal.agents.export') + onClick: _ctx.resetAgentsDiffState, + disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, + title: _ctx.t('common.backToEdit') }, [ (_openBlock(), _createElementBlock("svg", { class: "btn-icon-sm", @@ -6798,217 +6805,204 @@ return function render(_ctx, _cache) { stroke: "currentColor", "stroke-width": "2" }, [ - _createElementVNode("path", { d: "M12 3v12M8 11l4 4 4-4M4 17v2h16v-2" }) + _createElementVNode("path", { d: "M19 12H5M12 19l-7-7 7-7" }) ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]), - _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.copyAgentsContent, - disabled: _ctx.agentsLoading, - title: _ctx.t('modal.agents.copy') - }, [ - (_openBlock(), _createElementBlock("svg", { + ], 8 /* PROPS */, ["onClick", "disabled", "title"])) + : _createCommentVNode("v-if", true), + _createElementVNode("button", { + class: "btn-mini btn-confirm-mini", + onClick: _ctx.applyAgentsContent, + disabled: _ctx.agentsSaving || _ctx.agentsLoading || _ctx.agentsDiffLoading || (!_ctx.agentsDiffVisible && !_ctx.hasAgentsContentChanged()) || (_ctx.agentsDiffVisible && !_ctx.agentsDiffHasChanges), + title: _ctx.agentsDiffVisible ? _ctx.t('common.save') : _ctx.t('common.preview') + }, [ + (_ctx.agentsDiffVisible) + ? (_openBlock(), _createElementBlock("svg", { + key: 0, class: "btn-icon-sm", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ - _createElementVNode("rect", { - x: "9", - y: "9", - width: "13", - height: "13", - rx: "2" - }), - _createElementVNode("path", { d: "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" }) + _createElementVNode("path", { d: "M20 6L9 17l-5-5" }) ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]), - _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.pasteAgentsContent, - disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, - title: _ctx.t('common.paste') - }, [ - (_openBlock(), _createElementBlock("svg", { + : (_openBlock(), _createElementBlock("svg", { + key: 1, class: "btn-icon-sm", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ - _createElementVNode("rect", { - x: "8", - y: "2", - width: "8", - height: "4", - rx: "1" - }), - _createElementVNode("path", { d: "M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" }) + _createElementVNode("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), + _createElementVNode("circle", { + cx: "12", + cy: "12", + r: "3" + }) ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]) - ]), - _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--workflow" }, [ - _createElementVNode("button", { - class: "btn-mini", - onClick: $event => (_ctx.switchPromptsSubTab('presets')), - disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffLoading, - title: _ctx.t('prompts.presets.open') - }, _toDisplayString(_ctx.t('prompts.subTab.presets')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), - _createElementVNode("button", { - class: "btn-mini", - onClick: _ctx.loadPromptsContent, - disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, - title: _ctx.t('common.cancel') + ], 8 /* PROPS */, ["onClick", "disabled", "title"]) + ]) + ]) + ]), + _createElementVNode("details", { class: "prompt-presets-panel" }, [ + _createElementVNode("summary", { class: "prompt-presets-summary" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), + _createElementVNode("small", null, _toDisplayString(_ctx.t('prompts.presets.hint')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "prompts-editor-toolbar prompt-presets-toolbar" }, [ + _createElementVNode("div", { class: "prompts-editor-actions prompt-presets-save-row" }, [ + _withDirectives(_createElementVNode("input", { + type: "text", + class: "form-input prompt-presets-name-input", + "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), + placeholder: _ctx.t('prompts.presets.namePlaceholder'), + onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ + [_vModelText, _ctx.promptPresetNameDraft] + ]), + _createElementVNode("button", { + type: "button", + class: "btn-mini btn-confirm-mini", + onClick: _ctx.saveCurrentPromptAsPreset, + disabled: _ctx.promptPresetSaving || _ctx.agentsLoading + }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + ]) + ]), + _createElementVNode("div", { class: "form-group prompt-presets-body" }, [ + _createElementVNode("div", { class: "editor-frame prompt-presets-frame" }, [ + (!_ctx.promptPresets.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "state-message prompt-presets-empty" + }, _toDisplayString(_ctx.t('prompts.presets.empty')), 1 /* TEXT */)) + : (_openBlock(), _createElementBlock("div", { + key: 1, + class: "prompt-presets-list" }, [ - (_openBlock(), _createElementBlock("svg", { - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" - }, [ - _createElementVNode("path", { d: "M18 6L6 18M6 6l12 12" }) - ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]), - (_ctx.agentsDiffVisible) - ? (_openBlock(), _createElementBlock("button", { - key: 0, - class: "btn-mini", - onClick: _ctx.resetAgentsDiffState, - disabled: _ctx.agentsSaving || _ctx.agentsDiffLoading, - title: _ctx.t('common.backToEdit') + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { + return (_openBlock(), _createElementBlock("article", { + key: preset.id, + class: "prompt-preset-row" }, [ - (_openBlock(), _createElementBlock("svg", { - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" - }, [ - _createElementVNode("path", { d: "M19 12H5M12 19l-7-7 7-7" }) - ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"])) - : _createCommentVNode("v-if", true), - _createElementVNode("button", { - class: "btn-mini btn-confirm-mini", - onClick: _ctx.applyAgentsContent, - disabled: _ctx.agentsSaving || _ctx.agentsLoading || _ctx.agentsDiffLoading || (!_ctx.agentsDiffVisible && !_ctx.hasAgentsContentChanged()) || (_ctx.agentsDiffVisible && !_ctx.agentsDiffHasChanges), - title: _ctx.agentsDiffVisible ? _ctx.t('common.save') : _ctx.t('common.preview') - }, [ - (_ctx.agentsDiffVisible) - ? (_openBlock(), _createElementBlock("svg", { - key: 0, - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" - }, [ - _createElementVNode("path", { d: "M20 6L9 17l-5-5" }) - ])) - : (_openBlock(), _createElementBlock("svg", { - key: 1, - class: "btn-icon-sm", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2" - }, [ - _createElementVNode("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), - _createElementVNode("circle", { - cx: "12", - cy: "12", - r: "3" - }) - ])) - ], 8 /* PROPS */, ["onClick", "disabled", "title"]) - ]) - ]) - ]), - _createElementVNode("div", { class: "form-group" }, [ - (_ctx.agentsDiffVisible) - ? (_openBlock(), _createElementBlock("div", { key: 0 }, [ - (!_ctx.agentsDiffLoading && !_ctx.agentsDiffError && !_ctx.agentsDiffTruncated && (_ctx.agentsDiffStats.added || _ctx.agentsDiffStats.removed)) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "agents-diff-summary" - }, [ - _createElementVNode("span", { class: "agents-diff-stat add" }, "+" + _toDisplayString(_ctx.agentsDiffStats.added), 1 /* TEXT */), - _createElementVNode("span", { class: "agents-diff-stat del" }, "-" + _toDisplayString(_ctx.agentsDiffStats.removed), 1 /* TEXT */) - ])) - : _createCommentVNode("v-if", true), - (_ctx.agentsDiffLoading) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "state-message" - }, _toDisplayString(_ctx.t('diff.generating')), 1 /* TEXT */)) - : (_ctx.agentsDiffError) - ? (_openBlock(), _createElementBlock("div", { - key: 2, - class: "state-message error" - }, _toDisplayString(_ctx.agentsDiffError), 1 /* TEXT */)) - : (_ctx.agentsDiffTruncated) - ? (_openBlock(), _createElementBlock("div", { - key: 3, - class: "agents-diff-empty" - }, _toDisplayString(_ctx.t('diff.tooLargeSkip')), 1 /* TEXT */)) - : (!_ctx.agentsDiffHasChanges) - ? (_openBlock(), _createElementBlock("div", { - key: 4, - class: "agents-diff-empty" - }, _toDisplayString(_ctx.t('diff.noChanges')), 1 /* TEXT */)) - : (_openBlock(), _createElementBlock("div", { - key: 5, - class: "agents-diff-view agents-diff-editor" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.agentsDiffLines, (line, index) => { - return (_openBlock(), _createElementBlock("div", { - key: line.key || (line.type + '-' + index), - class: _normalizeClass(['agents-diff-line', line.type]) - }, [ - _createElementVNode("span", { class: "agents-diff-line-sign" }, _toDisplayString(line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ')), 1 /* TEXT */), - _createElementVNode("span", { class: "agents-diff-line-text" }, _toDisplayString(line.value), 1 /* TEXT */) - ], 2 /* CLASS */)) - }), 128 /* KEYED_FRAGMENT */)) - ])) + _createElementVNode("div", { class: "prompt-preset-main" }, [ + _createElementVNode("input", { + type: "text", + class: "form-input prompt-preset-name", + value: _ctx.getPromptPresetRenameDraft(preset), + onInput: $event => (_ctx.setPromptPresetRenameDraft(preset.id, $event.target.value)), + onKeydown: _withKeys(_withModifiers($event => (_ctx.renamePromptPreset(preset)), ["prevent"]), ["enter"]), + "aria-label": _ctx.t('prompts.presets.renameAria') + }, null, 40 /* PROPS, NEED_HYDRATION */, ["value", "onInput", "onKeydown", "aria-label"]), + _createElementVNode("div", { class: "form-hint" }, _toDisplayString(_ctx.t('prompts.presets.updatedAt', { time: _ctx.formatPromptPresetTime(preset.updatedAt) })), 1 /* TEXT */), + _createElementVNode("pre", { class: "prompt-preset-preview" }, _toDisplayString(preset.content), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "prompts-editor-actions prompt-preset-actions" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.renamePromptPreset(preset)) + }, _toDisplayString(_ctx.t('common.save')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini", + onClick: $event => (_ctx.applyPromptPresetToEditor(preset)) + }, _toDisplayString(_ctx.t('common.paste')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "btn-mini btn-danger-mini", + onClick: $event => (_ctx.deletePromptPreset(preset)) + }, _toDisplayString(_ctx.t('common.delete')), 9 /* TEXT, PROPS */, ["onClick"]) + ]) + ])) + }), 128 /* KEYED_FRAGMENT */)) ])) - : _createCommentVNode("v-if", true), - _createElementVNode("div", { - class: _normalizeClass(['editor-frame', { 'editor-frame--loading': _ctx.agentsLoading }]) - }, [ - (_ctx.agentsLoading) + ]) + ]) + ]), + _createElementVNode("div", { class: "form-group" }, [ + (_ctx.agentsDiffVisible) + ? (_openBlock(), _createElementBlock("div", { key: 0 }, [ + (!_ctx.agentsDiffLoading && !_ctx.agentsDiffError && !_ctx.agentsDiffTruncated && (_ctx.agentsDiffStats.added || _ctx.agentsDiffStats.removed)) ? (_openBlock(), _createElementBlock("div", { key: 0, - class: "editor-skeleton" + class: "agents-diff-summary" }, [ - (_openBlock(), _createElementBlock(_Fragment, null, _renderList(6, (i) => { - return _createElementVNode("div", { - class: "skeleton-line", - key: i - }) - }), 64 /* STABLE_FRAGMENT */)) + _createElementVNode("span", { class: "agents-diff-stat add" }, "+" + _toDisplayString(_ctx.agentsDiffStats.added), 1 /* TEXT */), + _createElementVNode("span", { class: "agents-diff-stat del" }, "-" + _toDisplayString(_ctx.agentsDiffStats.removed), 1 /* TEXT */) ])) : _createCommentVNode("v-if", true), - _withDirectives(_createElementVNode("textarea", { - "onUpdate:modelValue": $event => ((_ctx.agentsContent) = $event), - class: "form-input template-editor", - spellcheck: "false", - readonly: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, - onInput: _ctx.onAgentsContentInput, - placeholder: _ctx.t(_ctx.promptsSubTab === 'claude-project' ? 'modal.agents.placeholder.claudeProject' : 'modal.agents.placeholder') - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "readonly", "onInput", "placeholder"]), [ - [_vModelText, _ctx.agentsContent] - ]) - ], 2 /* CLASS */), - (_ctx.promptsContextHint) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: _normalizeClass(['prompts-context-hint', { 'prompts-context-hint--warn': _ctx.promptsContextHint.warn }]) - }, _toDisplayString(_ctx.promptsContextHint.text), 3 /* TEXT, CLASS */)) - : _createCommentVNode("v-if", true) + (_ctx.agentsDiffLoading) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "state-message" + }, _toDisplayString(_ctx.t('diff.generating')), 1 /* TEXT */)) + : (_ctx.agentsDiffError) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "state-message error" + }, _toDisplayString(_ctx.agentsDiffError), 1 /* TEXT */)) + : (_ctx.agentsDiffTruncated) + ? (_openBlock(), _createElementBlock("div", { + key: 3, + class: "agents-diff-empty" + }, _toDisplayString(_ctx.t('diff.tooLargeSkip')), 1 /* TEXT */)) + : (!_ctx.agentsDiffHasChanges) + ? (_openBlock(), _createElementBlock("div", { + key: 4, + class: "agents-diff-empty" + }, _toDisplayString(_ctx.t('diff.noChanges')), 1 /* TEXT */)) + : (_openBlock(), _createElementBlock("div", { + key: 5, + class: "agents-diff-view agents-diff-editor" + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.agentsDiffLines, (line, index) => { + return (_openBlock(), _createElementBlock("div", { + key: line.key || (line.type + '-' + index), + class: _normalizeClass(['agents-diff-line', line.type]) + }, [ + _createElementVNode("span", { class: "agents-diff-line-sign" }, _toDisplayString(line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ')), 1 /* TEXT */), + _createElementVNode("span", { class: "agents-diff-line-text" }, _toDisplayString(line.value), 1 /* TEXT */) + ], 2 /* CLASS */)) + }), 128 /* KEYED_FRAGMENT */)) + ])) + ])) + : _createCommentVNode("v-if", true), + _createElementVNode("div", { + class: _normalizeClass(['editor-frame', { 'editor-frame--loading': _ctx.agentsLoading }]) + }, [ + (_ctx.agentsLoading) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "editor-skeleton" + }, [ + (_openBlock(), _createElementBlock(_Fragment, null, _renderList(6, (i) => { + return _createElementVNode("div", { + class: "skeleton-line", + key: i + }) + }), 64 /* STABLE_FRAGMENT */)) + ])) + : _createCommentVNode("v-if", true), + _withDirectives(_createElementVNode("textarea", { + "onUpdate:modelValue": $event => ((_ctx.agentsContent) = $event), + class: "form-input template-editor", + spellcheck: "false", + readonly: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + onInput: _ctx.onAgentsContentInput, + placeholder: _ctx.t(_ctx.promptsSubTab === 'claude-project' ? 'modal.agents.placeholder.claudeProject' : 'modal.agents.placeholder') + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "readonly", "onInput", "placeholder"]), [ + [_vModelText, _ctx.agentsContent] ]) - ])) + ], 2 /* CLASS */), + (_ctx.promptsContextHint) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: _normalizeClass(['prompts-context-hint', { 'prompts-context-hint--warn': _ctx.promptsContextHint.warn }]) + }, _toDisplayString(_ctx.promptsContextHint.text), 3 /* TEXT, CLASS */)) + : _createCommentVNode("v-if", true) + ]) + ]) ])) : _createCommentVNode("v-if", true), _createCommentVNode(" 加载状态 "), diff --git a/web-ui/styles/modals-core.css b/web-ui/styles/modals-core.css index dfa183ef..aa18470f 100644 --- a/web-ui/styles/modals-core.css +++ b/web-ui/styles/modals-core.css @@ -727,22 +727,31 @@ } .prompt-presets-panel { - margin-top: 12px; + margin: 12px 0; + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-md); + background: var(--color-bg-card); } -.prompt-presets-toolbar .form-hint { +.prompt-presets-summary { display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.prompt-presets-toolbar .form-hint strong { + align-items: baseline; + gap: 10px; + padding: 10px 12px; + cursor: pointer; color: var(--color-text-primary); - font-size: var(--font-size-body); font-weight: var(--font-weight-primary); } +.prompt-presets-summary small { + color: var(--color-text-secondary); + font-weight: 400; +} + +.prompt-presets-toolbar { + padding: 0 12px 10px; +} + .prompt-presets-save-row { gap: 8px; } @@ -753,16 +762,20 @@ .prompt-presets-body { min-height: 0; + margin: 0; + padding: 0 12px 12px; } .prompt-presets-frame { - min-height: 75vh; + max-height: 320px; + min-height: 0; + overflow: auto; background: var(--color-bg-card); } .prompt-presets-empty { margin: 0; - min-height: 75vh; + min-height: 120px; display: flex; align-items: center; justify-content: center; From 63632faa065a8324fa150bdf3836758de1e5cea9 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 12 Jul 2026 03:09:12 +0000 Subject: [PATCH 11/14] fix(prompts): surface preset save in editor --- tests/unit/config-tabs-ui.test.mjs | 6 ++-- tests/unit/prompt-presets.test.mjs | 40 +++++++++++++++++++++--- web-ui/modules/app.methods.agents.mjs | 3 +- web-ui/partials/index/panel-prompts.html | 32 +++++++++---------- web-ui/res/web-ui-render.precompiled.js | 36 ++++++++++----------- 5 files changed, 73 insertions(+), 44 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 213ff101..a312d9b7 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -98,12 +98,12 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /class="form-input prompt-presets-select"/); assert.match(html, /@change="applyPromptPresetSelection\(\$event\)"/); assert.match(html, /t\('prompts\.presets\.selectPlaceholder'\)/); - assert.match(html, /class="prompts-editor-toolbar prompt-presets-toolbar"/); - assert.match(html, /class="prompts-editor-actions prompt-presets-save-row"/); + assert.match(html, /v-model="promptPresetNameDraft"/); + assert.match(html, /@keydown.enter.prevent="saveCurrentPromptAsPreset"/); + assert.match(html, /@click="saveCurrentPromptAsPreset"/); assert.match(html, /@click="applyPromptPresetToEditor\(preset\)"/); assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'codex'\)"/); assert.doesNotMatch(html, /@click="applyPromptPresetToEditor\(preset, 'claude-project'\)"/); - assert.doesNotMatch(html, /@click="saveEditorPromptAsPreset"/); assert.match(html, /class="form-group prompt-presets-body"/); assert.match(html, /class="editor-frame prompt-presets-frame"/); assert.match(html, /id="side-tab-orchestration"/); diff --git a/tests/unit/prompt-presets.test.mjs b/tests/unit/prompt-presets.test.mjs index 15358637..7f6411e9 100644 --- a/tests/unit/prompt-presets.test.mjs +++ b/tests/unit/prompt-presets.test.mjs @@ -65,21 +65,24 @@ function createVm(overrides = {}) { return { vm, apiCalls, persisted, messages, confirms }; } -test('prompt presets reject empty name and empty content', async () => { +test('prompt presets use current md name by default and reject empty content', async () => { const { vm, messages, persisted } = createVm({ promptPresetNameDraft: ' ', + agentsPath: '/tmp/work/AGENTS.md', agentsContent: 'usable content' }); await vm.saveCurrentPromptAsPreset(); - assert.equal(messages.at(-1).message, 'prompts.presets.error.emptyName'); - assert.deepEqual(persisted, []); + assert.equal(vm.promptPresets.length, 1); + assert.equal(vm.promptPresets[0].name, 'AGENTS.md'); + assert.equal(vm.promptPresets[0].content, 'usable content'); + assert.equal(persisted.length, 1); vm.promptPresetNameDraft = 'Preset'; vm.agentsContent = ' '; await vm.saveCurrentPromptAsPreset(); assert.equal(messages.at(-1).message, 'prompts.presets.error.emptyContent'); - assert.deepEqual(persisted, []); + assert.equal(persisted.length, 1); }); test('prompt presets save new item and overwrite duplicate only after confirmation', async () => { @@ -143,6 +146,35 @@ test('prompt presets surface preference persistence failures before success', as assert.notEqual(messages.at(-1)?.message, 'prompts.presets.toast.saved'); }); + +test('prompt presets keep user content as inert text for common security payloads', async () => { + const payloads = [ + `'; DROP TABLE prompt_presets; --`, + `" OR "1"="1`, + ``, + ``, + `../../../../etc/passwd`, + `..\\..\\windows\\win.ini`, + `file:///etc/passwd`, + `http://169.254.169.254/latest/meta-data/`, + `-----BEGIN PRIVATE KEY-----\nsecret`, + `Authorization: Bearer secret-token` + ]; + const { vm, apiCalls } = createVm({ + promptPresetNameDraft: `Security payloads`, + agentsContent: payloads.join('\n') + }); + + await vm.saveCurrentPromptAsPreset(); + assert.equal(vm.promptPresets.length, 1); + assert.equal(vm.promptPresets[0].content, payloads.join('\n')); + + vm.agentsContent = 'dirty'; + await vm.applyPromptPresetToEditor(vm.promptPresets[0]); + assert.equal(vm.agentsContent, payloads.join('\n')); + assert.deepEqual(apiCalls, []); +}); + test('prompt presets paste into current editor draft without target switch or file write', async () => { const { vm, apiCalls, confirms, messages } = createVm({ promptPresets: [{ id: 'p1', name: 'Base', content: 'preset body', updatedAt: '2026-01-01T00:00:00.000Z' }], diff --git a/web-ui/modules/app.methods.agents.mjs b/web-ui/modules/app.methods.agents.mjs index bf13c266..bfa075bc 100644 --- a/web-ui/modules/app.methods.agents.mjs +++ b/web-ui/modules/app.methods.agents.mjs @@ -785,7 +785,8 @@ export function createAgentsMethods(options = {}) { }, async saveCurrentPromptAsPreset() { if (this.promptPresetSaving || this.agentsLoading) return; - const name = this.normalizePromptPresetName(this.promptPresetNameDraft); + const draftName = this.normalizePromptPresetName(this.promptPresetNameDraft); + const name = draftName || this.normalizePromptPresetName(this.getCurrentPromptPresetDefaultName()); const content = typeof this.agentsContent === 'string' ? this.agentsContent : ''; if (!name) { this.showMessage(this.t('prompts.presets.error.emptyName'), 'error'); diff --git a/web-ui/partials/index/panel-prompts.html b/web-ui/partials/index/panel-prompts.html index d8ca25cd..a9f0e198 100644 --- a/web-ui/partials/index/panel-prompts.html +++ b/web-ui/partials/index/panel-prompts.html @@ -74,6 +74,21 @@ + + @@ -97,23 +112,6 @@ {{ t('prompts.presets.title') }} {{ t('prompts.presets.hint') }} -
-
- - -
-
{{ t('prompts.presets.empty') }}
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index c83375fd..ce482872 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -6774,6 +6774,23 @@ return function render(_ctx, _cache) { }, _toDisplayString(preset.name), 9 /* TEXT, PROPS */, ["value"])) }), 128 /* KEYED_FRAGMENT */)) ], 40 /* PROPS, NEED_HYDRATION */, ["disabled", "title", "onChange"]), + _withDirectives(_createElementVNode("input", { + type: "text", + class: "form-input prompt-presets-name-input", + "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), + placeholder: _ctx.t('prompts.presets.namePlaceholder'), + disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "disabled", "onKeydown"]), [ + [_vModelText, _ctx.promptPresetNameDraft] + ]), + _createElementVNode("button", { + type: "button", + class: "btn-mini btn-confirm-mini", + onClick: _ctx.saveCurrentPromptAsPreset, + disabled: _ctx.promptPresetSaving || _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + title: _ctx.t('prompts.presets.saveCurrent') + }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), _createElementVNode("button", { class: "btn-mini", onClick: _ctx.loadPromptsContent, @@ -6850,25 +6867,6 @@ return function render(_ctx, _cache) { _createElementVNode("span", null, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), _createElementVNode("small", null, _toDisplayString(_ctx.t('prompts.presets.hint')), 1 /* TEXT */) ]), - _createElementVNode("div", { class: "prompts-editor-toolbar prompt-presets-toolbar" }, [ - _createElementVNode("div", { class: "prompts-editor-actions prompt-presets-save-row" }, [ - _withDirectives(_createElementVNode("input", { - type: "text", - class: "form-input prompt-presets-name-input", - "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), - placeholder: _ctx.t('prompts.presets.namePlaceholder'), - onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ - [_vModelText, _ctx.promptPresetNameDraft] - ]), - _createElementVNode("button", { - type: "button", - class: "btn-mini btn-confirm-mini", - onClick: _ctx.saveCurrentPromptAsPreset, - disabled: _ctx.promptPresetSaving || _ctx.agentsLoading - }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) - ]) - ]), _createElementVNode("div", { class: "form-group prompt-presets-body" }, [ _createElementVNode("div", { class: "editor-frame prompt-presets-frame" }, [ (!_ctx.promptPresets.length) From 6aa8c9fc6312a7ecb0429160f0a70d3f54e93dc3 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 12 Jul 2026 03:16:39 +0000 Subject: [PATCH 12/14] fix(prompts): make preset save visible --- web-ui/partials/index/panel-prompts.html | 53 ++++++++++-------- web-ui/res/web-ui-render.precompiled.js | 69 +++++++++++++----------- web-ui/styles/modals-core.css | 33 ++++++++++++ web-ui/styles/responsive.css | 32 +++++++++++ 4 files changed, 133 insertions(+), 54 deletions(-) diff --git a/web-ui/partials/index/panel-prompts.html b/web-ui/partials/index/panel-prompts.html index a9f0e198..df2a3217 100644 --- a/web-ui/partials/index/panel-prompts.html +++ b/web-ui/partials/index/panel-prompts.html @@ -66,29 +66,6 @@
- - - @@ -106,6 +83,36 @@
+
+
+
+ + +
+
+ + +
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index ce482872..7e2e5d95 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -6760,37 +6760,6 @@ return function render(_ctx, _cache) { ], 8 /* PROPS */, ["onClick", "disabled", "title"]) ]), _createElementVNode("div", { class: "prompts-editor-group prompts-editor-group--workflow" }, [ - _createElementVNode("select", { - class: "form-input prompt-presets-select", - disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible || !_ctx.promptPresets.length, - title: _ctx.t('prompts.presets.title'), - onChange: $event => (_ctx.applyPromptPresetSelection($event)) - }, [ - _createElementVNode("option", { value: "" }, _toDisplayString(_ctx.t('prompts.presets.selectPlaceholder')), 1 /* TEXT */), - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { - return (_openBlock(), _createElementBlock("option", { - key: preset.id, - value: preset.id - }, _toDisplayString(preset.name), 9 /* TEXT, PROPS */, ["value"])) - }), 128 /* KEYED_FRAGMENT */)) - ], 40 /* PROPS, NEED_HYDRATION */, ["disabled", "title", "onChange"]), - _withDirectives(_createElementVNode("input", { - type: "text", - class: "form-input prompt-presets-name-input", - "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), - placeholder: _ctx.t('prompts.presets.namePlaceholder'), - disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, - onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "disabled", "onKeydown"]), [ - [_vModelText, _ctx.promptPresetNameDraft] - ]), - _createElementVNode("button", { - type: "button", - class: "btn-mini btn-confirm-mini", - onClick: _ctx.saveCurrentPromptAsPreset, - disabled: _ctx.promptPresetSaving || _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, - title: _ctx.t('prompts.presets.saveCurrent') - }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]), _createElementVNode("button", { class: "btn-mini", onClick: _ctx.loadPromptsContent, @@ -6862,6 +6831,44 @@ return function render(_ctx, _cache) { ]) ]) ]), + _createElementVNode("div", { class: "prompt-presets-inline-row" }, [ + _createElementVNode("div", { class: "prompt-presets-inline-group" }, [ + _createElementVNode("label", { class: "prompt-presets-inline-label" }, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), + _createElementVNode("select", { + class: "form-input prompt-presets-select", + disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible || !_ctx.promptPresets.length, + title: _ctx.t('prompts.presets.title'), + onChange: $event => (_ctx.applyPromptPresetSelection($event)) + }, [ + _createElementVNode("option", { value: "" }, _toDisplayString(_ctx.t('prompts.presets.selectPlaceholder')), 1 /* TEXT */), + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.promptPresets, (preset) => { + return (_openBlock(), _createElementBlock("option", { + key: preset.id, + value: preset.id + }, _toDisplayString(preset.name), 9 /* TEXT, PROPS */, ["value"])) + }), 128 /* KEYED_FRAGMENT */)) + ], 40 /* PROPS, NEED_HYDRATION */, ["disabled", "title", "onChange"]) + ]), + _createElementVNode("div", { class: "prompt-presets-inline-group prompt-presets-inline-group--save" }, [ + _withDirectives(_createElementVNode("input", { + type: "text", + class: "form-input prompt-presets-name-input", + "onUpdate:modelValue": $event => ((_ctx.promptPresetNameDraft) = $event), + placeholder: _ctx.t('prompts.presets.namePlaceholder'), + disabled: _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + onKeydown: _withKeys(_withModifiers(_ctx.saveCurrentPromptAsPreset, ["prevent"]), ["enter"]) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "disabled", "onKeydown"]), [ + [_vModelText, _ctx.promptPresetNameDraft] + ]), + _createElementVNode("button", { + type: "button", + class: "btn-mini btn-confirm-mini", + onClick: _ctx.saveCurrentPromptAsPreset, + disabled: _ctx.promptPresetSaving || _ctx.agentsLoading || _ctx.agentsSaving || _ctx.agentsDiffVisible, + title: _ctx.t('prompts.presets.saveCurrent') + }, _toDisplayString(_ctx.t('prompts.presets.saveCurrent')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]) + ]) + ]), _createElementVNode("details", { class: "prompt-presets-panel" }, [ _createElementVNode("summary", { class: "prompt-presets-summary" }, [ _createElementVNode("span", null, _toDisplayString(_ctx.t('prompts.presets.title')), 1 /* TEXT */), diff --git a/web-ui/styles/modals-core.css b/web-ui/styles/modals-core.css index aa18470f..29820cd3 100644 --- a/web-ui/styles/modals-core.css +++ b/web-ui/styles/modals-core.css @@ -726,6 +726,39 @@ box-shadow: none; } +.prompt-presets-inline-row { + display: flex; + align-items: center; + gap: 10px; + margin: 8px 0 12px; + padding: 10px 12px; + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-md); + background: var(--color-bg-card); +} + +.prompt-presets-inline-group { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.prompt-presets-inline-group--save { + flex: 1; + justify-content: flex-end; +} + +.prompt-presets-inline-label { + flex-shrink: 0; + color: var(--color-text-secondary); + font-size: var(--font-size-small); +} + +.prompt-presets-select { + min-width: 180px; +} + .prompt-presets-panel { margin: 12px 0; border: 1px solid var(--color-border-soft); diff --git a/web-ui/styles/responsive.css b/web-ui/styles/responsive.css index 16d891df..e744a403 100644 --- a/web-ui/styles/responsive.css +++ b/web-ui/styles/responsive.css @@ -199,6 +199,22 @@ textarea:focus-visible { flex-wrap: wrap; } + .prompt-presets-inline-row { + align-items: stretch; + flex-direction: column; + } + + .prompt-presets-inline-group, + .prompt-presets-inline-group--save { + width: 100%; + justify-content: flex-start; + } + + .prompt-presets-select, + .prompt-presets-name-input { + width: 100%; + } + .template-editor, .agents-diff-editor { min-height: min(50vh, 400px); @@ -604,6 +620,22 @@ textarea:focus-visible { width: 100%; flex-wrap: wrap; } + + .prompt-presets-inline-row { + align-items: stretch; + flex-direction: column; + } + + .prompt-presets-inline-group, + .prompt-presets-inline-group--save { + width: 100%; + justify-content: flex-start; + } + + .prompt-presets-select, + .prompt-presets-name-input { + width: 100%; + } } @media (max-width: 820px) { From c7f2ae2b4fedb773ea961c4c478a9a2d226f6ca4 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 12 Jul 2026 03:42:11 +0000 Subject: [PATCH 13/14] fix(prompts): shorten preset copy --- web-ui/modules/i18n/locales/en.mjs | 16 ++++++++-------- web-ui/modules/i18n/locales/ja.mjs | 16 ++++++++-------- web-ui/modules/i18n/locales/vi.mjs | 14 +++++++------- web-ui/modules/i18n/locales/zh-tw.mjs | 16 ++++++++-------- web-ui/modules/i18n/locales/zh.mjs | 16 ++++++++-------- 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index b1f13e66..82d1ed18 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -182,7 +182,7 @@ const en = Object.freeze({ 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude prompt file', 'side.prompts.presets': 'Preset pool', - 'side.prompts.presets.meta': 'Reusable prompt presets', + 'side.prompts.presets.meta': 'Save and paste presets', 'side.plugins.tools': 'Prompt Tools', 'side.plugins.tools.meta': 'Templates / Variables', 'side.plugins.templatesCount': '{count} templates', @@ -227,12 +227,12 @@ const en = Object.freeze({ 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', 'prompts.subTab.presets': 'Preset pool', - 'prompts.presets.title': 'Prompt preset pool', - 'prompts.presets.hint': 'Save reusable prompt content here, then apply it to AGENTS.md or CLAUDE.md. Applying a preset only updates the editor; click Save to write the file.', + 'prompts.presets.title': 'Presets', + 'prompts.presets.hint': 'Manage presets.', 'prompts.presets.namePlaceholder': 'Preset name', - 'prompts.presets.selectPlaceholder': 'Paste preset…', - 'prompts.presets.saveCurrent': 'Save current as preset', - 'prompts.presets.empty': 'No presets yet. Open AGENTS.md or CLAUDE.md, enter content, then save it as a preset.', + 'prompts.presets.selectPlaceholder': 'Choose preset…', + 'prompts.presets.saveCurrent': 'Save preset', + 'prompts.presets.empty': 'No presets yet', 'prompts.presets.updatedAt': 'Updated: {time}', 'prompts.presets.renameAria': 'Rename prompt preset', 'prompts.presets.open': 'Open preset pool', @@ -242,14 +242,14 @@ const en = Object.freeze({ 'prompts.presets.error.emptyContent': 'Preset content cannot be empty', 'prompts.presets.error.duplicateName': 'A preset with this name already exists', 'prompts.presets.confirm.addCurrentTitle': 'Add to preset pool?', - 'prompts.presets.confirm.addCurrentMessage': 'Add the current editor content to the preset pool as “{name}”. This will not save the md file.', + 'prompts.presets.confirm.addCurrentMessage': 'Save as “{name}”.', 'prompts.presets.confirm.overwriteTitle': 'Overwrite preset?', 'prompts.presets.confirm.overwriteMessage': 'A preset named “{name}” already exists. Overwrite it with the current editor content?', 'prompts.presets.confirm.overwriteConfirm': 'Overwrite', 'prompts.presets.confirm.deleteTitle': 'Delete preset?', 'prompts.presets.confirm.deleteMessage': 'Delete preset “{name}”? This cannot be undone.', 'prompts.presets.toast.saved': 'Preset saved', - 'prompts.presets.toast.pasted': 'Current editor content replaced. File not saved yet.', + 'prompts.presets.toast.pasted': 'Pasted, not saved.', 'prompts.presets.toast.renamed': 'Preset renamed', 'prompts.presets.toast.deleted': 'Preset deleted', 'prompts.project.pathLabel': 'Project path (optional)', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index d53107d0..d005bf39 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -183,7 +183,7 @@ const ja = Object.freeze({ 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude プロンプトファイル', 'side.prompts.presets': 'プリセットプール', - 'side.prompts.presets.meta': '再利用可能なプロンプトプリセット', + 'side.prompts.presets.meta': 'プリセットを保存・貼り付け', 'side.plugins.tools': 'プロンプトツール', 'side.plugins.tools.meta': 'テンプレート / 変数', 'side.plugins.templatesCount': '{count} 件のテンプレート', @@ -228,12 +228,12 @@ const ja = Object.freeze({ 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', 'prompts.subTab.presets': 'プリセット', - 'prompts.presets.title': 'Prompt 共通プリセット', - 'prompts.presets.hint': '再利用する prompt 内容を保存し、AGENTS.md または CLAUDE.md に適用できます。適用してもエディターだけが更新され、ファイルへ書き込むには保存が必要です。', + 'prompts.presets.title': 'プリセット', + 'prompts.presets.hint': 'プリセットを管理します。', 'prompts.presets.namePlaceholder': 'プリセット名', - 'prompts.presets.selectPlaceholder': 'プリセットを選んでペースト…', - 'prompts.presets.saveCurrent': '現在の内容をプリセット保存', - 'prompts.presets.empty': 'プリセットはまだありません。AGENTS.md または CLAUDE.md を開いて内容を編集し、プリセットとして保存してください。', + 'prompts.presets.selectPlaceholder': 'プリセットを選択…', + 'prompts.presets.saveCurrent': 'プリセット保存', + 'prompts.presets.empty': 'プリセットはありません', 'prompts.presets.updatedAt': '更新: {time}', 'prompts.presets.renameAria': 'prompt プリセット名を変更', 'prompts.presets.open': 'プリセットを開く', @@ -243,14 +243,14 @@ const ja = Object.freeze({ 'prompts.presets.error.emptyContent': 'プリセット内容は空にできません', 'prompts.presets.error.duplicateName': '同じ名前のプリセットが既に存在します', 'prompts.presets.confirm.addCurrentTitle': 'プリセットプールに追加しますか?', - 'prompts.presets.confirm.addCurrentMessage': '現在のエディター内容を「{name}」としてプリセットプールに追加します。md ファイルは保存されません。', + 'prompts.presets.confirm.addCurrentMessage': '「{name}」として保存します。', 'prompts.presets.confirm.overwriteTitle': 'プリセットを上書きしますか?', 'prompts.presets.confirm.overwriteMessage': '「{name}」というプリセットは既に存在します。現在のエディター内容で上書きしますか?', 'prompts.presets.confirm.overwriteConfirm': '上書き', 'prompts.presets.confirm.deleteTitle': 'プリセットを削除しますか?', 'prompts.presets.confirm.deleteMessage': 'プリセット「{name}」を削除しますか?この操作は元に戻せません。', 'prompts.presets.toast.saved': 'プリセットを保存しました', - 'prompts.presets.toast.pasted': '現在のエディター内容を置き換えました。ファイルはまだ保存されていません。', + 'prompts.presets.toast.pasted': '貼り付けました。未保存です。', 'prompts.presets.toast.renamed': 'プリセット名を変更しました', 'prompts.presets.toast.deleted': 'プリセットを削除しました', 'prompts.project.pathLabel': 'プロジェクトパス(任意)', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index f7b167f0..69919c48 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -242,12 +242,12 @@ const vi = Object.freeze({ 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', 'prompts.subTab.presets': 'Kho preset', - 'prompts.presets.title': 'Kho preset prompt dùng chung', - 'prompts.presets.hint': 'Lưu nội dung prompt tái sử dụng tại đây, rồi áp dụng vào AGENTS.md hoặc CLAUDE.md. Áp dụng preset chỉ cập nhật editor; cần bấm Lưu để ghi tệp.', + 'prompts.presets.title': 'Preset', + 'prompts.presets.hint': 'Quản lý preset.', 'prompts.presets.namePlaceholder': 'Tên preset', - 'prompts.presets.selectPlaceholder': 'Chọn preset để dán…', - 'prompts.presets.saveCurrent': 'Lưu nội dung hiện tại thành preset', - 'prompts.presets.empty': 'Chưa có preset. Mở AGENTS.md hoặc CLAUDE.md, nhập nội dung rồi lưu thành preset.', + 'prompts.presets.selectPlaceholder': 'Chọn preset…', + 'prompts.presets.saveCurrent': 'Lưu preset', + 'prompts.presets.empty': 'Chưa có preset', 'prompts.presets.updatedAt': 'Cập nhật: {time}', 'prompts.presets.renameAria': 'Đổi tên preset prompt', 'prompts.presets.open': 'Mở kho preset', @@ -257,14 +257,14 @@ const vi = Object.freeze({ 'prompts.presets.error.emptyContent': 'Nội dung preset không được để trống', 'prompts.presets.error.duplicateName': 'Đã có preset trùng tên', 'prompts.presets.confirm.addCurrentTitle': 'Thêm vào kho preset?', - 'prompts.presets.confirm.addCurrentMessage': 'Thêm nội dung trình soạn thảo hiện tại vào kho preset với tên “{name}”. Thao tác này không lưu tệp md.', + 'prompts.presets.confirm.addCurrentMessage': 'Lưu thành “{name}”.', 'prompts.presets.confirm.overwriteTitle': 'Ghi đè preset?', 'prompts.presets.confirm.overwriteMessage': 'Preset “{name}” đã tồn tại. Ghi đè bằng nội dung editor hiện tại?', 'prompts.presets.confirm.overwriteConfirm': 'Ghi đè', 'prompts.presets.confirm.deleteTitle': 'Xóa preset?', 'prompts.presets.confirm.deleteMessage': 'Xóa preset “{name}”? Không thể hoàn tác.', 'prompts.presets.toast.saved': 'Đã lưu preset', - 'prompts.presets.toast.pasted': 'Đã thay nội dung trình soạn thảo hiện tại. Tệp chưa được lưu.', + 'prompts.presets.toast.pasted': 'Đã dán, chưa lưu.', 'prompts.presets.toast.renamed': 'Đã đổi tên preset', 'prompts.presets.toast.deleted': 'Đã xóa preset', 'prompts.project.pathLabel': 'Đường dẫn dự án (tùy chọn)', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index d4d187cb..bc7c2a1a 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -182,7 +182,7 @@ const zhTw = Object.freeze({ 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude 指令文件', 'side.prompts.presets': '預設池', - 'side.prompts.presets.meta': '複用 Prompt 預設', + 'side.prompts.presets.meta': '儲存和貼上預設', 'side.plugins.tools': '提示詞工具', 'side.plugins.tools.meta': '模板 / 變量', 'side.plugins.templatesCount': '{count} 個模板', @@ -227,12 +227,12 @@ const zhTw = Object.freeze({ 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', 'prompts.subTab.presets': '預設池', - 'prompts.presets.title': 'Prompt 通用預設池', - 'prompts.presets.hint': '把可重用的 prompt 內容保存在這裡,再套用到 AGENTS.md 或 CLAUDE.md。套用預設只更新編輯器,仍需點擊保存才會寫入文件。', + 'prompts.presets.title': '預設池', + 'prompts.presets.hint': '管理預設。', 'prompts.presets.namePlaceholder': '預設名稱', - 'prompts.presets.selectPlaceholder': '選擇預設並貼上…', - 'prompts.presets.saveCurrent': '保存目前內容為預設', - 'prompts.presets.empty': '暫無預設。先打開 AGENTS.md 或 CLAUDE.md 編輯內容,再保存為預設。', + 'prompts.presets.selectPlaceholder': '選擇預設…', + 'prompts.presets.saveCurrent': '存為預設', + 'prompts.presets.empty': '暫無預設', 'prompts.presets.updatedAt': '更新時間:{time}', 'prompts.presets.renameAria': '重新命名 prompt 預設', 'prompts.presets.open': '打開預設池', @@ -242,14 +242,14 @@ const zhTw = Object.freeze({ 'prompts.presets.error.emptyContent': '預設內容不能為空', 'prompts.presets.error.duplicateName': '已存在同名預設', 'prompts.presets.confirm.addCurrentTitle': '加入預設池?', - 'prompts.presets.confirm.addCurrentMessage': '將目前編輯器內容以「{name}」加入預設池。不會保存 md 檔案。', + 'prompts.presets.confirm.addCurrentMessage': '存為「{name}」。', 'prompts.presets.confirm.overwriteTitle': '覆蓋預設?', 'prompts.presets.confirm.overwriteMessage': '名為「{name}」的預設已存在。要用目前編輯器內容覆蓋它嗎?', 'prompts.presets.confirm.overwriteConfirm': '覆蓋', 'prompts.presets.confirm.deleteTitle': '刪除預設?', 'prompts.presets.confirm.deleteMessage': '刪除預設「{name}」?此操作不可復原。', 'prompts.presets.toast.saved': '預設已保存', - 'prompts.presets.toast.pasted': '已替換目前編輯區內容,尚未保存文件。', + 'prompts.presets.toast.pasted': '已貼上,未儲存。', 'prompts.presets.toast.renamed': '預設已重新命名', 'prompts.presets.toast.deleted': '預設已刪除', 'prompts.project.pathLabel': '專案路徑(可選)', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index 1ca19314..e68368d9 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -182,7 +182,7 @@ const zh = Object.freeze({ 'side.prompts.claude': 'CLAUDE.md', 'side.prompts.claude.meta': 'Claude 指令文件', 'side.prompts.presets': '预设池', - 'side.prompts.presets.meta': '复用 Prompt 预设', + 'side.prompts.presets.meta': '保存和粘贴预设', 'side.plugins.tools': '提示词工具', 'side.plugins.tools.meta': '模板 / 变量', 'side.plugins.templatesCount': '{count} 个模板', @@ -227,12 +227,12 @@ const zh = Object.freeze({ 'prompts.subTab.codex': 'AGENTS.md (Codex)', 'prompts.subTab.project': 'CLAUDE.md', 'prompts.subTab.presets': '预设池', - 'prompts.presets.title': 'Prompt 通用预设池', - 'prompts.presets.hint': '把可复用的 prompt 内容保存在这里,再应用到 AGENTS.md 或 CLAUDE.md。应用预设只更新编辑器,仍需点击保存才会写入文件。', + 'prompts.presets.title': '预设池', + 'prompts.presets.hint': '管理预设。', 'prompts.presets.namePlaceholder': '预设名称', - 'prompts.presets.selectPlaceholder': '选择预设并粘贴…', - 'prompts.presets.saveCurrent': '保存当前内容为预设', - 'prompts.presets.empty': '暂无预设。先打开 AGENTS.md 或 CLAUDE.md 编辑内容,再保存为预设。', + 'prompts.presets.selectPlaceholder': '选择预设…', + 'prompts.presets.saveCurrent': '存为预设', + 'prompts.presets.empty': '暂无预设', 'prompts.presets.updatedAt': '更新时间:{time}', 'prompts.presets.renameAria': '重命名 prompt 预设', 'prompts.presets.open': '打开预设池', @@ -242,14 +242,14 @@ const zh = Object.freeze({ 'prompts.presets.error.emptyContent': '预设内容不能为空', 'prompts.presets.error.duplicateName': '已存在同名预设', 'prompts.presets.confirm.addCurrentTitle': '添加到预设池?', - 'prompts.presets.confirm.addCurrentMessage': '将当前编辑器内容作为“{name}”添加到预设池。不会保存 md 文件。', + 'prompts.presets.confirm.addCurrentMessage': '保存为“{name}”。', 'prompts.presets.confirm.overwriteTitle': '覆盖预设?', 'prompts.presets.confirm.overwriteMessage': '名为“{name}”的预设已存在。要用当前编辑器内容覆盖它吗?', 'prompts.presets.confirm.overwriteConfirm': '覆盖', 'prompts.presets.confirm.deleteTitle': '删除预设?', 'prompts.presets.confirm.deleteMessage': '删除预设“{name}”?此操作不可撤销。', 'prompts.presets.toast.saved': '预设已保存', - 'prompts.presets.toast.pasted': '已替换当前编辑区内容,尚未保存文件。', + 'prompts.presets.toast.pasted': '已粘贴,未保存。', 'prompts.presets.toast.renamed': '预设已重命名', 'prompts.presets.toast.deleted': '预设已删除', 'prompts.project.pathLabel': '项目路径(可选)', From afe6355df61652ea56e32fd0bf9d38b2a81b436a Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 12 Jul 2026 03:56:49 +0000 Subject: [PATCH 14/14] chore(release): bump version to 0.1.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6bbe5a6..aac2688a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codexmate", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codexmate", - "version": "0.1.0", + "version": "0.1.1", "license": "Apache-2.0", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 38cf12e4..1cc10ada 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codexmate", - "version": "0.1.0", + "version": "0.1.1", "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具", "main": "cli.js", "bin": {