feat(prompt): add custom output format sequences#182
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough新增“Prompt 输出格式”功能:新数据库表 prompt_output_format_items 与 PromptOutputFormatDB,配套 IPC 通道/处理器/preload/渲染服务,Prompt store 状态与动作,新的 PromptOutputFormatPanel 面板与详情入口,复制流程支持多 Prompt 队列拼接,备份导入导出支持该数据,多语言文案与 spec 文档,及相应单元测试。 ChangesPrompt 输出格式功能
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Port the Prompt output format contribution from jazzson51569/PromptHub commit fa7b0e6 into a clean upstream branch. Refs: jazzson51569/PromptHub@fa7b0e6b Verification:\n- pnpm typecheck\n- pnpm --filter @prompthub/desktop test -- tests/unit/main/prompt-output-format-db.test.ts tests/unit/components/prompt-detail-metadata.test.tsx --run Co-authored-by: jazzson51569 <jazzson51569@153.com>
9947235 to
0bb8657
Compare
Refs: #182 Verification: - pnpm typecheck - pnpm --filter @prompthub/desktop test -- tests/unit/main/prompt-output-format-db.test.ts tests/unit/services/database-backup-format.test.ts tests/unit/services/database-backup.test.ts --run - git diff --check
PR Summary by QodoAdd persistent prompt output-format sequences (multi-prompt copy)
AI Description
Diagram
High-Level Assessment
Files changed (34)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
23 rules 1. Skipped-details string not translated
|
| skipped.outputFormatItems > 0 | ||
| ? `output format items: ${skipped.outputFormatItems}` | ||
| : null, |
There was a problem hiding this comment.
1. Skipped-details string not translated 📘 Rule violation ✧ Quality
formatImportSkippedDetails() and the import preview/summary counts line both build user-facing text with hardcoded English labels (including the newly added output format items) instead of using t(...). This creates partially untranslated UI content (even when embedded in a translated toast) and violates the react-i18next localization requirement.
Agent Prompt
## Issue description
User-facing import details and summary text are constructed with hardcoded English labels (including `output format items`) in `formatImportSkippedDetails()` and in the import preview/summary JSX, resulting in partially untranslated UI and non-compliance with the requirement to use react-i18next `t(...)` for user-visible strings.
## Issue Context
`formatImportSkippedDetails()` produces a details string that is interpolated into `t("toast.importPartialSuccess", { details: ... })` and displayed via a toast in `confirmImport`, so any interpolated fragments must also be localized. Separately, `BackupImportConfirmDialog` renders an import summary/counts sentence directly in the UI and must be fully localizable rather than mixing translated and hardcoded English nouns.
## Fix Focus Areas
- apps/desktop/src/renderer/hooks/useBackupImportController.tsx[22-39]
- apps/desktop/src/renderer/hooks/useBackupImportController.tsx[99-104]
- apps/desktop/src/renderer/components/settings/BackupImportConfirmDialog.tsx[43-43]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| createOutputFormatItem: async (data) => { | ||
| const item = await db.createOutputFormatItem(data); | ||
| set((state) => ({ | ||
| outputFormatItems: [ | ||
| item, | ||
| ...state.outputFormatItems.filter((existing) => existing.id !== item.id), | ||
| ], | ||
| })); |
There was a problem hiding this comment.
2. Output format order breaks 🐞 Bug ≡ Correctness
createOutputFormatItem prepends the newly created item into outputFormatItems, but PromptOutputFormatPanel renders and computes drag/drop indices from the array order without sorting by sortOrder, so new items (typically last by sortOrder) can appear at the top and reorder operations can send incorrect newSortOrder values.
Agent Prompt
### Issue description
`outputFormatItems` is treated as already ordered by `sortOrder` in the Output Format panel, but the store currently prepends created items. This makes the rendered list order diverge from persisted `sortOrder`, and drag-and-drop reorder uses `findIndex()` on this unsorted list to compute the `newSortOrder`, causing incorrect reorders.
### Issue Context
- DB `list()` is ordered by `sort_order ASC, created_at ASC`.
- The panel does not sort before rendering/reordering.
### Fix
Make `outputFormatItems` ordering consistent everywhere:
1) In the store, after `createOutputFormatItem`, either:
- call `fetchOutputFormatItems()` (preferred for correctness), **or**
- insert the new item into the array in `sortOrder` order.
2) Additionally (defense-in-depth), sort the panel’s `formatItems` by `item.sortOrder` then `item.createdAt` before rendering and before computing `targetIndex`.
### Fix Focus Areas
- apps/desktop/src/renderer/stores/prompt.store.ts[234-264]
- apps/desktop/src/renderer/components/prompt/PromptOutputFormatPanel.tsx[41-58]
- apps/desktop/src/renderer/components/prompt/PromptOutputFormatPanel.tsx[183-207]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const existingTargetIds = useMemo(() => { | ||
| const ids = new Set<string>(); | ||
| for (const item of formatItems) { | ||
| if (item.item.targetPromptId) { | ||
| ids.add(item.item.targetPromptId); | ||
| } else { | ||
| ids.add(currentPrompt.id); | ||
| } | ||
| } | ||
| return ids; | ||
| }, [currentPrompt.id, formatItems]); | ||
|
|
||
| const candidatePrompts = useMemo( | ||
| () => prompts.filter((prompt) => !existingTargetIds.has(prompt.id)), | ||
| [existingTargetIds, prompts], | ||
| ); | ||
|
|
||
| const searchResults = useMemo(() => { | ||
| if (!query.trim()) return []; | ||
| const lowerQuery = query.toLowerCase(); | ||
| return candidatePrompts.filter( | ||
| (prompt) => | ||
| prompt.title.toLowerCase().includes(lowerQuery) || | ||
| prompt.description?.toLowerCase().includes(lowerQuery) || | ||
| prompt.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)), | ||
| ); | ||
| }, [candidatePrompts, query]); | ||
|
|
||
| const handleAddSelf = useCallback(async () => { | ||
| if (savingTargetId) return; | ||
| if (!currentPrompt.id) return; | ||
| setSavingTargetId("self"); | ||
| try { | ||
| await onCreateOutputFormatItem({ | ||
| sourcePromptId: currentPrompt.id, | ||
| targetPromptId: null, | ||
| }); | ||
| } finally { | ||
| setSavingTargetId(null); | ||
| } | ||
| }, [currentPrompt.id, onCreateOutputFormatItem, savingTargetId]); | ||
|
|
||
| const handleAddPrompt = useCallback( | ||
| async (targetPromptId: string) => { | ||
| if (savingTargetId) return; | ||
| if (!currentPrompt.id || !targetPromptId) return; | ||
| setSavingTargetId(targetPromptId); | ||
| try { | ||
| const hasSelf = existingTargetIds.has(currentPrompt.id); | ||
| if (!hasSelf) { | ||
| await onCreateOutputFormatItem({ | ||
| sourcePromptId: currentPrompt.id, | ||
| targetPromptId: null, | ||
| }); | ||
| } | ||
| await onCreateOutputFormatItem({ | ||
| sourcePromptId: currentPrompt.id, | ||
| targetPromptId, | ||
| }); | ||
| setQuery(""); |
There was a problem hiding this comment.
3. Self selectable as target 🐞 Bug ≡ Correctness
PromptOutputFormatPanel does not always exclude currentPrompt.id from search candidates, so users can add the current prompt as targetPromptId, producing a non-NULL self-target row that is rendered as a normal (non-self) entry and can duplicate the source prompt in the configured sequence.
Agent Prompt
### Issue description
The output format panel can allow adding the current prompt as a target prompt (i.e., `targetPromptId === currentPrompt.id`) when no self item exists yet. This creates redundant/ambiguous “self” entries because the UI only treats `targetPromptId === null` as self.
### Issue Context
- `candidatePrompts` only filters by `existingTargetIds`, which does not include `currentPrompt.id` until a NULL-target self row exists.
- DB `normalizeCreateInput()` does not coerce or reject `targetPromptId === sourcePromptId`.
### Fix
Implement both UI and DB guardrails:
1) In `PromptOutputFormatPanel`, exclude the current prompt from `candidatePrompts` unconditionally (e.g., `prompt.id !== currentPrompt.id`).
2) In `PromptOutputFormatDB.normalizeCreateInput`, treat `targetPromptId === sourcePromptId` as `null` (or throw) to enforce the intended representation of “self”.
### Fix Focus Areas
- apps/desktop/src/renderer/components/prompt/PromptOutputFormatPanel.tsx[87-146]
- packages/db/src/prompt-output-format.ts[172-186]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/services/database-backup.ts (1)
750-793: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
importDatabaseViaMainProcess未检查createOutputFormatAPI 可用性函数顶部的可用性检查(Lines 750-758)未包含
window.api?.prompt?.createOutputFormat。当该 API 不可用时(例如版本不匹配),函数会继续执行:删除现有 prompts/folders → 插入新数据 → 调用createOutputFormatItem时抛出异常。这导致现有数据已删除、新数据部分恢复,但 outputFormatItems 创建失败,且非主进程回退路径不会执行。将
createOutputFormat加入可用性检查后,函数会返回false,回退到非主进程路径(Lines 935-949),该路径有 try/catch 可优雅处理失败。🔒 建议修复:添加 createOutputFormat 到可用性检查
if ( !window.api?.prompt?.getAll || !window.api?.prompt?.delete || !window.api?.prompt?.insertDirect || + !window.api?.prompt?.createOutputFormat || !window.api?.folder?.getAll || !window.api?.folder?.delete || !window.api?.folder?.insertDirect || !window.api?.version?.insertDirect ) { return false; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/services/database-backup.ts` around lines 750 - 793, `importDatabaseViaMainProcess` 的前置可用性检查缺少 `window.api?.prompt?.createOutputFormat`,导致在该 API 不可用时仍继续执行恢复流程并可能在 `createOutputFormatItem` 处失败。请在函数开头的能力检查中补上 `createOutputFormat`,让不兼容版本直接返回 `false`,从而回退到带 try/catch 的非主进程恢复路径,避免在 `existingPrompts`、`existingFolders` 清理后出现部分恢复状态。
🧹 Nitpick comments (2)
apps/desktop/src/renderer/components/prompt/PromptOutputFormatPanel.tsx (1)
373-383: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win条目重排仅通过 HTML5 拖拽实现,没有键盘可达的替代方式(拖拽手柄为
aria-hidden,行容器无role/键盘处理)。键盘用户可以添加/删除但无法重排。建议后续补充键盘重排(例如上/下移动按钮或方向键处理),使该交互对键盘/辅助技术可用。As per coding guidelines: "All interactive elements must have appropriate ARIA labels".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/components/prompt/PromptOutputFormatPanel.tsx` around lines 373 - 383, The item reordering in PromptOutputFormatPanel is only available via HTML5 drag-and-drop, leaving keyboard and assistive-technology users unable to reorder items. Update the draggable row in PromptOutputFormatPanel to provide a keyboard-accessible alternative, such as explicit move up/down controls or keyboard handlers on the item container, and ensure any interactive controls have appropriate ARIA labels while keeping the existing onDragStart/onDrop behavior intact.Source: Coding guidelines
packages/db/src/prompt-output-format.ts (1)
62-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
update方法(仅更新sort_order)在测试套件中完全没有被覆盖到,包括getById返回 null 的分支与data.sortOrder !== undefined ? … : existing.sortOrder两个分支。作为数据库边界模块,建议补充针对update的单元测试。As per coding guidelines: critical boundary modules (database …) "require 100% branch and condition coverage for changed behavior before merging".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/db/src/prompt-output-format.ts` around lines 62 - 81, The `update` method in `PromptOutputFormat` has changed behavior but lacks test coverage for both the `getById` null path and the `data.sortOrder !== undefined ? ... : existing.sortOrder` branch. Add unit tests for `update` that verify it returns null when `getById` finds no record, and that it preserves `existing.sortOrder` when `sortOrder` is omitted while using the provided value when present. Ensure the tests exercise the database boundary behavior through `update`, `getById`, and the `prompt_output_format_items` update path.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/desktop/src/main/ipc/prompt.ipc.ts`:
- Around line 313-344: The new output-format IPC handlers in prompt.ipc.ts need
upfront validation and error handling before touching outputFormatDb. Add input
checks in the PROMPT_OUTPUT_FORMAT_CREATE/UPDATE/DELETE/REORDER handlers (and
reuse the same style as assertPromptMoveInput) to verify ids are non-empty
strings, payloads are objects, and sort orders are finite non-negative numbers,
then reject malformed requests early. Wrap each ipcMain.handle callback in
try/catch so DB exceptions are converted into structured error responses instead
of propagating silently; keep the syncWorkspace calls only after successful
mutations.
In `@apps/desktop/src/renderer/components/settings/BackupImportConfirmDialog.tsx`:
- Line 43: The BackupImportConfirmDialog render text still hardcodes the "output
format items" label instead of routing it through i18n. Update the JSX in
BackupImportConfirmDialog to use t() for that label, following the existing
translation pattern used around importPreview.summary.counts, and add or reuse a
translation key so all user-visible text in this component is localized
consistently.
In `@apps/desktop/src/renderer/hooks/useBackupImportController.tsx`:
- Around line 29-31: The user-visible label in formatImportSkippedDetails is
hardcoded English and must be localized. Update the helper so the “output format
items” text goes through translation by either passing t into
formatImportSkippedDetails or using i18next.t() inside it, and keep the existing
skipped.outputFormatItems check unchanged. Make sure the returned string list in
useBackupImportController uses translated text for this label rather than a
literal.
In `@packages/db/src/prompt-output-format.ts`:
- Around line 83-120: The reorder flow in prompt-output-format’s reorder method
performs multiple UPDATE statements without atomic protection, so wrap the shift
UPDATE and final item UPDATE in db.transaction() to keep sort_order consistent
if any step fails. Apply the same transaction pattern to the multi-statement
delete path and normalizeSortOrder helper as well, using their existing method
names to locate the write sequences.
---
Outside diff comments:
In `@apps/desktop/src/renderer/services/database-backup.ts`:
- Around line 750-793: `importDatabaseViaMainProcess` 的前置可用性检查缺少
`window.api?.prompt?.createOutputFormat`,导致在该 API 不可用时仍继续执行恢复流程并可能在
`createOutputFormatItem` 处失败。请在函数开头的能力检查中补上 `createOutputFormat`,让不兼容版本直接返回
`false`,从而回退到带 try/catch 的非主进程恢复路径,避免在 `existingPrompts`、`existingFolders`
清理后出现部分恢复状态。
---
Nitpick comments:
In `@apps/desktop/src/renderer/components/prompt/PromptOutputFormatPanel.tsx`:
- Around line 373-383: The item reordering in PromptOutputFormatPanel is only
available via HTML5 drag-and-drop, leaving keyboard and assistive-technology
users unable to reorder items. Update the draggable row in
PromptOutputFormatPanel to provide a keyboard-accessible alternative, such as
explicit move up/down controls or keyboard handlers on the item container, and
ensure any interactive controls have appropriate ARIA labels while keeping the
existing onDragStart/onDrop behavior intact.
In `@packages/db/src/prompt-output-format.ts`:
- Around line 62-81: The `update` method in `PromptOutputFormat` has changed
behavior but lacks test coverage for both the `getById` null path and the
`data.sortOrder !== undefined ? ... : existing.sortOrder` branch. Add unit tests
for `update` that verify it returns null when `getById` finds no record, and
that it preserves `existing.sortOrder` when `sortOrder` is omitted while using
the provided value when present. Ensure the tests exercise the database boundary
behavior through `update`, `getById`, and the `prompt_output_format_items`
update path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b78c102a-1377-4443-9d14-74275b904f3b
📒 Files selected for processing (34)
apps/desktop/src/main/database/index.tsapps/desktop/src/main/ipc/index.tsapps/desktop/src/main/ipc/prompt.ipc.tsapps/desktop/src/preload/api/prompt.tsapps/desktop/src/renderer/components/layout/MainContent.tsxapps/desktop/src/renderer/components/prompt/PromptDetailMetadata.tsxapps/desktop/src/renderer/components/prompt/PromptOutputFormatPanel.tsxapps/desktop/src/renderer/components/settings/BackupImportConfirmDialog.tsxapps/desktop/src/renderer/hooks/useBackupImportController.tsxapps/desktop/src/renderer/i18n/locales/de.jsonapps/desktop/src/renderer/i18n/locales/en.jsonapps/desktop/src/renderer/i18n/locales/es.jsonapps/desktop/src/renderer/i18n/locales/fr.jsonapps/desktop/src/renderer/i18n/locales/ja.jsonapps/desktop/src/renderer/i18n/locales/zh-TW.jsonapps/desktop/src/renderer/i18n/locales/zh.jsonapps/desktop/src/renderer/services/database-backup-format.tsapps/desktop/src/renderer/services/database-backup.tsapps/desktop/src/renderer/services/database.tsapps/desktop/src/renderer/stores/prompt.store.tsapps/desktop/tests/unit/components/data-settings.test.tsxapps/desktop/tests/unit/main/prompt-output-format-db.test.tsapps/desktop/tests/unit/services/database-backup-format.test.tsapps/desktop/tests/unit/services/database-backup.test.tspackages/db/src/index.tspackages/db/src/prompt-output-format.tspackages/db/src/schema.tspackages/shared/constants/ipc-channels.tspackages/shared/types/prompt.tsspec/changes/active/prompt-output-format-contribution/design.mdspec/changes/active/prompt-output-format-contribution/implementation.mdspec/changes/active/prompt-output-format-contribution/proposal.mdspec/changes/active/prompt-output-format-contribution/specs/prompt/spec.mdspec/changes/active/prompt-output-format-contribution/tasks.md
|
|
||
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => { | ||
| const item = outputFormatDb.create(data); | ||
| syncWorkspace(); | ||
| return item; | ||
| }); | ||
|
|
||
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => { | ||
| return outputFormatDb.list(query); | ||
| }); | ||
|
|
||
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => { | ||
| const item = outputFormatDb.update(id, data); | ||
| if (item) { | ||
| syncWorkspace(); | ||
| } | ||
| return item; | ||
| }); | ||
|
|
||
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => { | ||
| const deleted = outputFormatDb.delete(id); | ||
| if (deleted) { | ||
| syncWorkspace(); | ||
| } | ||
| return deleted; | ||
| }); | ||
|
|
||
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => { | ||
| outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder); | ||
| syncWorkspace(); | ||
| return true; | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
IPC 处理器缺少输入校验,违反编码规范。
新增的五个输出格式 IPC 处理器均未验证输入参数。根据 apps/desktop/src/main/ipc/** 编码规范:"All IPC handlers must validate input types and reject malformed payloads before processing" 以及 "IPC handlers must catch errors and return structured error responses, never crash the main process silently"。
具体问题:
- CREATE:未校验
data是否为对象、sourcePromptId是否为非空字符串、targetPromptId是否为string|null、sortOrder是否为非负数。 - REORDER:未校验
sourcePromptId/itemId为非空字符串,未校验newSortOrder为有限非负数(对比同文件中assertPromptMoveInput对newOrder的校验)。 - UPDATE/DELETE:未校验
id为非空字符串。 - 所有处理器均无 try/catch,DB 层异常直接传播为 IPC rejection。
As per coding guidelines: apps/desktop/src/main/ipc/** — "All IPC handlers must validate input types and reject malformed payloads before processing" and "IPC handlers must catch errors and return structured error responses, never crash the main process silently".
🛡️ 建议添加输入校验函数
+ const assertOutputFormatId = (id: string) => {
+ if (typeof id !== 'string' || id.trim().length === 0) {
+ throw new Error('Output format item id is required');
+ }
+ };
+
+ const assertOutputFormatCreateInput = (data: CreateOutputFormatItemDTO) => {
+ if (!data || typeof data !== 'object') {
+ throw new Error('Output format data is required');
+ }
+ if (typeof data.sourcePromptId !== 'string' || data.sourcePromptId.trim().length === 0) {
+ throw new Error('Source prompt id is required');
+ }
+ if (
+ data.targetPromptId !== null &&
+ (typeof data.targetPromptId !== 'string' || data.targetPromptId.trim().length === 0)
+ ) {
+ throw new Error('Target prompt id must be null or a non-empty string');
+ }
+ if (
+ data.sortOrder !== undefined &&
+ (!Number.isFinite(data.sortOrder) || data.sortOrder < 0)
+ ) {
+ throw new Error('Sort order must be a non-negative number');
+ }
+ };
+
+ const assertOutputFormatReorderInput = (
+ sourcePromptId: string,
+ itemId: string,
+ newSortOrder: number,
+ ) => {
+ if (typeof sourcePromptId !== 'string' || sourcePromptId.trim().length === 0) {
+ throw new Error('Source prompt id is required');
+ }
+ if (typeof itemId !== 'string' || itemId.trim().length === 0) {
+ throw new Error('Item id is required');
+ }
+ if (!Number.isFinite(newSortOrder) || newSortOrder < 0) {
+ throw new Error('Sort order must be a non-negative number');
+ }
+ };
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => {
+ assertOutputFormatCreateInput(data);
const item = outputFormatDb.create(data);
syncWorkspace();
return item;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => {
+ if (query !== undefined && (typeof query !== 'object' || query === null)) {
+ throw new Error('Query must be an object or undefined');
+ }
return outputFormatDb.list(query);
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => {
+ assertOutputFormatId(id);
+ if (!data || typeof data !== 'object') {
+ throw new Error('Update data is required');
+ }
+ if (
+ data.sortOrder !== undefined &&
+ (!Number.isFinite(data.sortOrder) || data.sortOrder < 0)
+ ) {
+ throw new Error('Sort order must be a non-negative number');
+ }
const item = outputFormatDb.update(id, data);
if (item) {
syncWorkspace();
}
return item;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => {
+ assertOutputFormatId(id);
const deleted = outputFormatDb.delete(id);
if (deleted) {
syncWorkspace();
}
return deleted;
});
ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => {
+ assertOutputFormatReorderInput(sourcePromptId, itemId, newSortOrder);
outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder);
syncWorkspace();
return true;
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => { | |
| const item = outputFormatDb.create(data); | |
| syncWorkspace(); | |
| return item; | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => { | |
| return outputFormatDb.list(query); | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => { | |
| const item = outputFormatDb.update(id, data); | |
| if (item) { | |
| syncWorkspace(); | |
| } | |
| return item; | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => { | |
| const deleted = outputFormatDb.delete(id); | |
| if (deleted) { | |
| syncWorkspace(); | |
| } | |
| return deleted; | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => { | |
| outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder); | |
| syncWorkspace(); | |
| return true; | |
| }); | |
| const assertOutputFormatId = (id: string) => { | |
| if (typeof id !== 'string' || id.trim().length === 0) { | |
| throw new Error('Output format item id is required'); | |
| } | |
| }; | |
| const assertOutputFormatCreateInput = (data: CreateOutputFormatItemDTO) => { | |
| if (!data || typeof data !== 'object') { | |
| throw new Error('Output format data is required'); | |
| } | |
| if (typeof data.sourcePromptId !== 'string' || data.sourcePromptId.trim().length === 0) { | |
| throw new Error('Source prompt id is required'); | |
| } | |
| if ( | |
| data.targetPromptId !== null && | |
| (typeof data.targetPromptId !== 'string' || data.targetPromptId.trim().length === 0) | |
| ) { | |
| throw new Error('Target prompt id must be null or a non-empty string'); | |
| } | |
| if ( | |
| data.sortOrder !== undefined && | |
| (!Number.isFinite(data.sortOrder) || data.sortOrder < 0) | |
| ) { | |
| throw new Error('Sort order must be a non-negative number'); | |
| } | |
| }; | |
| const assertOutputFormatReorderInput = ( | |
| sourcePromptId: string, | |
| itemId: string, | |
| newSortOrder: number, | |
| ) => { | |
| if (typeof sourcePromptId !== 'string' || sourcePromptId.trim().length === 0) { | |
| throw new Error('Source prompt id is required'); | |
| } | |
| if (typeof itemId !== 'string' || itemId.trim().length === 0) { | |
| throw new Error('Item id is required'); | |
| } | |
| if (!Number.isFinite(newSortOrder) || newSortOrder < 0) { | |
| throw new Error('Sort order must be a non-negative number'); | |
| } | |
| }; | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_CREATE, async (_, data: CreateOutputFormatItemDTO) => { | |
| assertOutputFormatCreateInput(data); | |
| const item = outputFormatDb.create(data); | |
| syncWorkspace(); | |
| return item; | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_LIST, async (_, query?: OutputFormatItemQuery) => { | |
| if (query !== undefined && (typeof query !== 'object' || query === null)) { | |
| throw new Error('Query must be an object or undefined'); | |
| } | |
| return outputFormatDb.list(query); | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_UPDATE, async (_, id: string, data: UpdateOutputFormatItemDTO) => { | |
| assertOutputFormatId(id); | |
| if (!data || typeof data !== 'object') { | |
| throw new Error('Update data is required'); | |
| } | |
| if ( | |
| data.sortOrder !== undefined && | |
| (!Number.isFinite(data.sortOrder) || data.sortOrder < 0) | |
| ) { | |
| throw new Error('Sort order must be a non-negative number'); | |
| } | |
| const item = outputFormatDb.update(id, data); | |
| if (item) { | |
| syncWorkspace(); | |
| } | |
| return item; | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_DELETE, async (_, id: string) => { | |
| assertOutputFormatId(id); | |
| const deleted = outputFormatDb.delete(id); | |
| if (deleted) { | |
| syncWorkspace(); | |
| } | |
| return deleted; | |
| }); | |
| ipcMain.handle(IPC_CHANNELS.PROMPT_OUTPUT_FORMAT_REORDER, async (_, sourcePromptId: string, itemId: string, newSortOrder: number) => { | |
| assertOutputFormatReorderInput(sourcePromptId, itemId, newSortOrder); | |
| outputFormatDb.reorder(sourcePromptId, itemId, newSortOrder); | |
| syncWorkspace(); | |
| return true; | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/desktop/src/main/ipc/prompt.ipc.ts` around lines 313 - 344, The new
output-format IPC handlers in prompt.ipc.ts need upfront validation and error
handling before touching outputFormatDb. Add input checks in the
PROMPT_OUTPUT_FORMAT_CREATE/UPDATE/DELETE/REORDER handlers (and reuse the same
style as assertPromptMoveInput) to verify ids are non-empty strings, payloads
are objects, and sort orders are finite non-negative numbers, then reject
malformed requests early. Wrap each ipcMain.handle callback in try/catch so DB
exceptions are converted into structured error responses instead of propagating
silently; keep the syncWorkspace calls only after successful mutations.
Source: Coding guidelines
| </p> | ||
| <p> | ||
| {t("settings.importPreviewCounts", "Will import")}: {importPreview.summary.counts.prompts} prompts, {importPreview.summary.counts.folders} folders, {importPreview.summary.counts.versions} versions, {importPreview.summary.counts.rules} rules, {importPreview.summary.counts.skills} skills | ||
| {t("settings.importPreviewCounts", "Will import")}: {importPreview.summary.counts.prompts} prompts, {importPreview.summary.counts.folders} folders, {importPreview.summary.counts.versions} versions, {importPreview.summary.counts.outputFormatItems} output format items, {importPreview.summary.counts.rules} rules, {importPreview.summary.counts.skills} skills |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
"output format items" 文本未通过 i18n 翻译
根据编码规范,渲染层所有用户可见文本必须通过 t() 翻译。新增的 "output format items" 标签为硬编码英文。虽然现有标签("prompts"、"folders" 等)也存在同样问题,但新增代码不应继续违反此规范。
As per coding guidelines: "{apps/desktop/src/renderer/,apps/web/src/client/}: All text visible to users must go through t() from react-i18next; this includes button labels, error messages, placeholders, tooltips, and status text"
🌐 建议修复:使用 t() 翻译标签
- {t("settings.importPreviewCounts", "Will import")}: {importPreview.summary.counts.prompts} prompts, {importPreview.summary.counts.folders} folders, {importPreview.summary.counts.versions} versions, {importPreview.summary.counts.outputFormatItems} output format items, {importPreview.summary.counts.rules} rules, {importPreview.summary.counts.skills} skills
+ {t("settings.importPreviewCounts", "Will import")}: {importPreview.summary.counts.prompts} {t("settings.importPreviewPrompts", "prompts")}, {importPreview.summary.counts.folders} {t("settings.importPreviewFolders", "folders")}, {importPreview.summary.counts.versions} {t("settings.importPreviewVersions", "versions")}, {importPreview.summary.counts.outputFormatItems} {t("settings.importPreviewOutputFormatItems", "output format items")}, {importPreview.summary.counts.rules} {t("settings.importPreviewRules", "rules")}, {importPreview.summary.counts.skills} {t("settings.importPreviewSkills", "skills")}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {t("settings.importPreviewCounts", "Will import")}: {importPreview.summary.counts.prompts} prompts, {importPreview.summary.counts.folders} folders, {importPreview.summary.counts.versions} versions, {importPreview.summary.counts.outputFormatItems} output format items, {importPreview.summary.counts.rules} rules, {importPreview.summary.counts.skills} skills | |
| {t("settings.importPreviewCounts", "Will import")}: {importPreview.summary.counts.prompts} {t("settings.importPreviewPrompts", "prompts")}, {importPreview.summary.counts.folders} {t("settings.importPreviewFolders", "folders")}, {importPreview.summary.counts.versions} {t("settings.importPreviewVersions", "versions")}, {importPreview.summary.counts.outputFormatItems} {t("settings.importPreviewOutputFormatItems", "output format items")}, {importPreview.summary.counts.rules} {t("settings.importPreviewRules", "rules")}, {importPreview.summary.counts.skills} {t("settings.importPreviewSkills", "skills")} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/desktop/src/renderer/components/settings/BackupImportConfirmDialog.tsx`
at line 43, The BackupImportConfirmDialog render text still hardcodes the
"output format items" label instead of routing it through i18n. Update the JSX
in BackupImportConfirmDialog to use t() for that label, following the existing
translation pattern used around importPreview.summary.counts, and add or reuse a
translation key so all user-visible text in this component is localized
consistently.
Source: Coding guidelines
| skipped.outputFormatItems > 0 | ||
| ? `output format items: ${skipped.outputFormatItems}` | ||
| : null, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
formatImportSkippedDetails 中 "output format items" 为硬编码英文
该函数返回的用户可见字符串中,"output format items" 标签未通过 t() 翻译。由于此函数是普通工具函数(非 React 组件),需将 t 作为参数传入或改用 i18next.t() 才能修复。
As per coding guidelines: "{apps/desktop/src/renderer/,apps/web/src/client/}: All text visible to users must go through t() from react-i18next"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/desktop/src/renderer/hooks/useBackupImportController.tsx` around lines
29 - 31, The user-visible label in formatImportSkippedDetails is hardcoded
English and must be localized. Update the helper so the “output format items”
text goes through translation by either passing t into
formatImportSkippedDetails or using i18next.t() inside it, and keep the existing
skipped.outputFormatItems check unchanged. Make sure the returned string list in
useBackupImportController uses translated text for this label rather than a
literal.
Source: Coding guidelines
| reorder(sourcePromptId: string, itemId: string, newSortOrder: number): void { | ||
| const items = this.list({ sourcePromptId }); | ||
| const itemIndex = items.findIndex((item) => item.id === itemId); | ||
| if (itemIndex === -1) return; | ||
|
|
||
| const item = items[itemIndex]; | ||
| const oldOrder = item.sortOrder; | ||
|
|
||
| if (newSortOrder === oldOrder) return; | ||
|
|
||
| const now = Date.now(); | ||
|
|
||
| if (newSortOrder > oldOrder) { | ||
| this.db | ||
| .prepare( | ||
| `UPDATE prompt_output_format_items | ||
| SET sort_order = sort_order - 1, updated_at = ? | ||
| WHERE source_prompt_id = ? AND sort_order > ? AND sort_order <= ?`, | ||
| ) | ||
| .run(now, sourcePromptId, oldOrder, newSortOrder); | ||
| } else { | ||
| this.db | ||
| .prepare( | ||
| `UPDATE prompt_output_format_items | ||
| SET sort_order = sort_order + 1, updated_at = ? | ||
| WHERE source_prompt_id = ? AND sort_order >= ? AND sort_order < ?`, | ||
| ) | ||
| .run(now, sourcePromptId, newSortOrder, oldOrder); | ||
| } | ||
|
|
||
| this.db | ||
| .prepare( | ||
| `UPDATE prompt_output_format_items | ||
| SET sort_order = ?, updated_at = ? | ||
| WHERE id = ?`, | ||
| ) | ||
| .run(newSortOrder, now, itemId); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
reorder 的多条写操作未包裹事务,存在原子性风险。
reorder 先执行一条位移 UPDATE,再执行一条设置目标 sort_order 的 UPDATE;delete(Line 149-162)在 DELETE 之后调用 normalizeSortOrder,而 normalizeSortOrder(Line 225-240)又在循环里多次 UPDATE。这些多语句写操作若中途失败,会残留错乱的 sort_order。请统一用 db.transaction() 包裹。
🔒 建议为 reorder 包裹事务(delete/normalizeSortOrder 同理)
reorder(sourcePromptId: string, itemId: string, newSortOrder: number): void {
const items = this.list({ sourcePromptId });
const itemIndex = items.findIndex((item) => item.id === itemId);
if (itemIndex === -1) return;
const item = items[itemIndex];
const oldOrder = item.sortOrder;
if (newSortOrder === oldOrder) return;
const now = Date.now();
- if (newSortOrder > oldOrder) {
- this.db
- .prepare(
- `UPDATE prompt_output_format_items
- SET sort_order = sort_order - 1, updated_at = ?
- WHERE source_prompt_id = ? AND sort_order > ? AND sort_order <= ?`,
- )
- .run(now, sourcePromptId, oldOrder, newSortOrder);
- } else {
- this.db
- .prepare(
- `UPDATE prompt_output_format_items
- SET sort_order = sort_order + 1, updated_at = ?
- WHERE source_prompt_id = ? AND sort_order >= ? AND sort_order < ?`,
- )
- .run(now, sourcePromptId, newSortOrder, oldOrder);
- }
-
- this.db
- .prepare(
- `UPDATE prompt_output_format_items
- SET sort_order = ?, updated_at = ?
- WHERE id = ?`,
- )
- .run(newSortOrder, now, itemId);
+ this.db.transaction(() => {
+ if (newSortOrder > oldOrder) {
+ this.db
+ .prepare(
+ `UPDATE prompt_output_format_items
+ SET sort_order = sort_order - 1, updated_at = ?
+ WHERE source_prompt_id = ? AND sort_order > ? AND sort_order <= ?`,
+ )
+ .run(now, sourcePromptId, oldOrder, newSortOrder);
+ } else {
+ this.db
+ .prepare(
+ `UPDATE prompt_output_format_items
+ SET sort_order = sort_order + 1, updated_at = ?
+ WHERE source_prompt_id = ? AND sort_order >= ? AND sort_order < ?`,
+ )
+ .run(now, sourcePromptId, newSortOrder, oldOrder);
+ }
+
+ this.db
+ .prepare(
+ `UPDATE prompt_output_format_items
+ SET sort_order = ?, updated_at = ?
+ WHERE id = ?`,
+ )
+ .run(newSortOrder, now, itemId);
+ })();
}As per coding guidelines: "Any operation involving multiple SQL statements must be wrapped in db.transaction()".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| reorder(sourcePromptId: string, itemId: string, newSortOrder: number): void { | |
| const items = this.list({ sourcePromptId }); | |
| const itemIndex = items.findIndex((item) => item.id === itemId); | |
| if (itemIndex === -1) return; | |
| const item = items[itemIndex]; | |
| const oldOrder = item.sortOrder; | |
| if (newSortOrder === oldOrder) return; | |
| const now = Date.now(); | |
| if (newSortOrder > oldOrder) { | |
| this.db | |
| .prepare( | |
| `UPDATE prompt_output_format_items | |
| SET sort_order = sort_order - 1, updated_at = ? | |
| WHERE source_prompt_id = ? AND sort_order > ? AND sort_order <= ?`, | |
| ) | |
| .run(now, sourcePromptId, oldOrder, newSortOrder); | |
| } else { | |
| this.db | |
| .prepare( | |
| `UPDATE prompt_output_format_items | |
| SET sort_order = sort_order + 1, updated_at = ? | |
| WHERE source_prompt_id = ? AND sort_order >= ? AND sort_order < ?`, | |
| ) | |
| .run(now, sourcePromptId, newSortOrder, oldOrder); | |
| } | |
| this.db | |
| .prepare( | |
| `UPDATE prompt_output_format_items | |
| SET sort_order = ?, updated_at = ? | |
| WHERE id = ?`, | |
| ) | |
| .run(newSortOrder, now, itemId); | |
| } | |
| reorder(sourcePromptId: string, itemId: string, newSortOrder: number): void { | |
| const items = this.list({ sourcePromptId }); | |
| const itemIndex = items.findIndex((item) => item.id === itemId); | |
| if (itemIndex === -1) return; | |
| const item = items[itemIndex]; | |
| const oldOrder = item.sortOrder; | |
| if (newSortOrder === oldOrder) return; | |
| const now = Date.now(); | |
| this.db.transaction(() => { | |
| if (newSortOrder > oldOrder) { | |
| this.db | |
| .prepare( | |
| `UPDATE prompt_output_format_items | |
| SET sort_order = sort_order - 1, updated_at = ? | |
| WHERE source_prompt_id = ? AND sort_order > ? AND sort_order <= ?`, | |
| ) | |
| .run(now, sourcePromptId, oldOrder, newSortOrder); | |
| } else { | |
| this.db | |
| .prepare( | |
| `UPDATE prompt_output_format_items | |
| SET sort_order = sort_order + 1, updated_at = ? | |
| WHERE source_prompt_id = ? AND sort_order >= ? AND sort_order < ?`, | |
| ) | |
| .run(now, sourcePromptId, newSortOrder, oldOrder); | |
| } | |
| this.db | |
| .prepare( | |
| `UPDATE prompt_output_format_items | |
| SET sort_order = ?, updated_at = ? | |
| WHERE id = ?`, | |
| ) | |
| .run(newSortOrder, now, itemId); | |
| })(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/db/src/prompt-output-format.ts` around lines 83 - 120, The reorder
flow in prompt-output-format’s reorder method performs multiple UPDATE
statements without atomic protection, so wrap the shift UPDATE and final item
UPDATE in db.transaction() to keep sort_order consistent if any step fails.
Apply the same transaction pattern to the multi-statement delete path and
normalizeSortOrder helper as well, using their existing method names to locate
the write sequences.
Source: Coding guidelines
Summary
jazzson51569/PromptHub@fa7b0e6binto a clean upstream branch.Scope Notes
This intentionally does not merge the contributor fork branch directly. The fork branch contains unrelated build/debug/data-path/package changes, so this PR cherry-picks the product feature only.
Verification
pnpm typecheckpnpm --filter @prompthub/desktop test -- tests/unit/main/prompt-output-format-db.test.ts tests/unit/components/prompt-detail-metadata.test.tsx --runCo-authored-by: jazzson51569 jazzson51569@153.com
Summary by CodeRabbit
新功能
Bug 修复