Skip to content

refactor(data): migrate QuickPhrase to Prompt with version management#13430

Open
zhangjiadi225 wants to merge 5 commits intoCherryHQ:v2from
zhangjiadi225:refactor/v2/prompt-management
Open

refactor(data): migrate QuickPhrase to Prompt with version management#13430
zhangjiadi225 wants to merge 5 commits intoCherryHQ:v2from
zhangjiadi225:refactor/v2/prompt-management

Conversation

@zhangjiadi225
Copy link

What this PR does

Before this PR:

  • Quick phrases stored in Dexie (IndexedDB) with no version history
  • No way to compare or rollback prompt changes
  • Unused template variable definition system (variables field) added complexity

After this PR:

  • Prompts stored in SQLite via Drizzle ORM with automatic version management
  • Content changes auto-create version snapshots; rollback supported
  • Quick panel supports version sub-menu for multi-version prompts
  • Removed unused variables system, keeping only ${var} syntax in content

Why we need it and why it was done in this way

The following tradeoffs were made:

  • Rollback creates a new version (append-only history) rather than reverting — ensures no history is ever lost
  • Version selection in quick panel is temporary (doesn't change currentVersion) — keeps version management in settings page

The following alternatives were considered:

  • Eager loading versions for all prompts — rejected in favor of lazy loading for performance

Breaking changes

Data migration from Dexie quick_phrases to SQLite prompt table runs automatically via PromptMigrator.

Checklist

  • PR: The PR description is expressive enough
  • Code: Write code that humans can understand and Keep it simple
  • Refactor: Boy Scout Rule followed
  • Documentation: Not required (internal data layer change)
  • Self-review

Release note

Migrated Quick Phrases to Prompt Management system with automatic version history, rollback support, and version selection in the quick panel.

- Replace QuickPhrase with Prompt entity backed by SQLite via Drizzle ORM
- Add prompt and prompt_version tables with automatic versioning
- Implement PromptService with CRUD, version history, and rollback
- Add API handlers and schemas for /prompts endpoints
- Create PromptMigrator for Dexie quick_phrases → SQLite migration
- Refactor QuickPhrasesButton with version sub-menu selection
- Replace QuickPhraseSettings with PromptSettings page
- Remove unused template variables system and templateEngine utility
- Update routing from /settings/quickphrase to /settings/prompts

Signed-off-by: zhangjiadi225 <625013594@qq.com>
@zhangjiadi225 zhangjiadi225 changed the base branch from main to v2 March 13, 2026 03:05
@zhangjiadi225 zhangjiadi225 requested a review from a team March 13, 2026 03:05
@DeJeune
Copy link
Collaborator

DeJeune commented Mar 13, 2026

Note

This comment was translated by Claude.

Add unit tests for the key migrator


Original Content 关键的migrator加一下单元测试

- Test prepare phase: table existence, valid/invalid filtering, empty table, error handling
- Test execute phase: insertion, title defaults, progress reporting, transaction errors
- Test validate phase: count matching, mismatch detection, db failure handling, skip tracking

Signed-off-by: zhangjiadi225 <625013594@qq.com>
@SiinXu
Copy link
Collaborator

SiinXu commented Mar 13, 2026

Nice work on the migration and version management! A few things before detailed review:

Question: Variable design

The old QuickPhrase had a variables field (unused), and the current ${var} syntax in content is inserted as-is with no runtime substitution. Since this PR is redesigning the Prompt data model:

Have you considered the variable substitution story? How should ${var} placeholders work going forward?

Want to make sure the data model accounts for this before we merge.

Initial code observations

  • Transaction safety: create(), update(), rollback(), and reorder() in PromptService are not wrapped in db.transaction() — risk of data inconsistency. The migrator correctly uses transactions, service methods should too.
  • UNIQUE constraint: The composite index on prompt_version(promptId, version) should be uniqueIndex() to enforce version uniqueness at DB level.
  • Nullable columns: currentVersion and sortOrder should have .notNull() constraints.

@zhangjiadi225
Copy link
Author

zhangjiadi225 commented Mar 13, 2026 via email

…aints

- Wrap create/update/rollback/reorder in db.transaction()
- Move reads inside transactions to prevent race conditions
- Add notNull constraints to currentVersion and sortOrder
- Change prompt_version composite index to uniqueIndex
- Remove redundant null fallbacks in rowToPrompt

Signed-off-by: zhangjiadi225 <625013594@qq.com>
Copy link
Collaborator

@EurFelux EurFelux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fully reviewed yet.

- Merge 2 migrations into 1 clean migration file
- Add Zod schemas for Prompt types and DTO validation
- Validate request body with Zod .parse() in handlers
- Remove redundant await and return promises directly

Signed-off-by: zhangjiadi225 <625013594@qq.com>
@EurFelux EurFelux self-requested a review March 18, 2026 07:54
…ttings

Remove all styled-components definitions from PromptSettings page and replace with Tailwind CSS utility classes to align with v2 UI refactoring requirements.
Comment on lines +21 to +22
createdAt: z.string(),
updatedAt: z.string()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use z.iso namespace and see what you want.

// ============================================================================

export const PromptSchema = z.object({
id: z.string(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider more strict z.uuid. Also other id fields if you want them to be uuid

Comment on lines +37 to +38
/** Whether this item comes from the assistant's regularPhrases */
isAssistantPhrase: boolean
Copy link
Collaborator

@EurFelux EurFelux Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to use a type field for extensibility.

is it really necessary to discriminate them?

Comment on lines +47 to +60
sources: {
electronStore: { get: vi.fn() },
reduxState: {
getCategory: vi.fn(),
getAllCategories: vi.fn()
} as unknown as MigrationContext['sources']['reduxState'],
dexieExport: {
tableExists: vi.fn().mockResolvedValue(tableExists),
readTable: vi.fn().mockResolvedValue(tableData),
getExportPath: vi.fn().mockReturnValue('/tmp/export'),
createStreamReader: vi.fn(),
getTableFileSize: vi.fn()
} as unknown as MigrationContext['sources']['dexieExport']
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI Blocker: createMockContext is missing dexieSettings and localStorage from sources, causing the typecheck to fail:

error TS2739: Type '{ electronStore: ...; reduxState: ...; dexieExport: ... }'
is missing the following properties: dexieSettings, localStorage

Need to add mock stubs for both:

dexieSettings: {
  get: vi.fn(),
  getAll: vi.fn()
} as unknown as MigrationContext['sources']['dexieSettings'],
localStorage: {
  get: vi.fn(),
  getAll: vi.fn()
} as unknown as MigrationContext['sources']['localStorage']

Comment on lines 378 to +381
)
}

const Label = styled.div`
const VarLabel = styled.div`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styled-components is being deprecated in v2 per kangfenmao's review. This VarLabel should be replaced with a TailwindCSS class like in PromptSettings.tsx:

// Replace VarLabel usage with:
<div className="mb-1 text-(--color-text) text-sm">...</div>

await loadPrompts()
}

const reversedPrompts = [...promptsList].reverse()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: reversedPrompts creates a new reversed array on every render. Consider memoizing it:

const reversedPrompts = useMemo(() => [...promptsList].reverse(), [promptsList])

Comment on lines +63 to +76
content: p.content,
isAssistantPhrase: true,
currentVersion: 1
}))

const globalPrompts: UnifiedPromptItem[] = prompts.map((p) => ({
id: p.id,
title: p.title,
content: p.content,
isAssistantPhrase: false,
currentVersion: p.currentVersion
}))

setPromptItems([...assistantPhrases, ...globalPrompts])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assistant.regularPhrases is still read from / written to Redux (not migrated to SQLite). This means:

  1. Assistant-level phrases remain in the old Redux store while global phrases move to SQLite — two different storage backends for conceptually the same data type
  2. The "Add to assistant" flow (line ~165) still calls updateAssistant({ ...assistant, regularPhrases: updatedPhrases }) which writes directly to Redux

Is this intentional? If so, it should be documented as a known limitation / follow-up. If not, consider unifying storage by adding a relation table, e.g.:

CREATE TABLE assistant_prompt (
  assistant_id TEXT NOT NULL REFERENCES assistant(id) ON DELETE CASCADE,
  prompt_id    TEXT NOT NULL REFERENCES prompt(id) ON DELETE CASCADE,
  sort_order   INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (assistant_id, prompt_id)
);

This way all prompts live in the prompt table (single source of truth), and the relation table maps which prompts belong to which assistant. Global prompts are simply those with no relation entry.

Comment on lines 79 to +81
useEffect(() => {
loadQuickListPhrases()
}, [loadQuickListPhrases])
loadPromptItems()
}, [loadPromptItems])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have useDataApi hook. Don't use useEffect to fetch data.

Comment on lines +33 to +40
const loadPrompts = useCallback(async () => {
const data = await dataApiService.get('/prompts')
setPromptsList(data)
}, [])

useEffect(() => {
loadPrompts()
}, [loadPrompts])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use useDataApi instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants