From 5f0d1623ae80fcc3426e028860ae3a3a00865872 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 7 Apr 2026 22:51:14 +0300 Subject: [PATCH] feat: expand magic prompts coverage and split generation prompts (#835) - Add configurable visible/instructions prompt families for commit/PR generation, PR checks/comments flows, and git conflict resolution helpers. - Refactor prompt sending to explicit visible + synthetic parts instead of newline-based splitting, with legacy override migration for old keys. - Polish Magic Prompts settings UX with grouped sidebar entries, tooltip-based descriptions, AI icon, and validation that visible prompts cannot be empty across web and VS Code runtimes. --- .../magic-prompts/MagicPromptsPage.tsx | 322 +++++++++++ .../magic-prompts/MagicPromptsSidebar.tsx | 73 +++ .../session/GitHubIssuePickerDialog.tsx | 41 +- .../session/GitHubPrPickerDialog.tsx | 48 +- .../components/session/NewWorktreeDialog.tsx | 89 +-- .../ui/src/components/views/SettingsView.tsx | 12 +- .../components/views/git/ConflictDialog.tsx | 46 +- .../views/git/IntegrateCommitsSection.tsx | 40 +- .../views/git/PullRequestSection.tsx | 32 +- packages/ui/src/lib/gitApi.ts | 73 +-- packages/ui/src/lib/magicPrompts.ts | 526 ++++++++++++++++++ packages/ui/src/lib/settings/metadata.ts | 8 + .../ui/src/stores/useMagicPromptsStore.ts | 20 + packages/vscode/src/bridge-config-runtime.ts | 36 ++ .../vscode/src/bridge-settings-runtime.ts | 101 ++++ packages/vscode/src/bridge.ts | 14 +- packages/vscode/webview/main.tsx | 24 + .../web/server/lib/magic-prompts/routes.js | 63 +++ .../web/server/lib/magic-prompts/runtime.js | 119 ++++ .../web/server/lib/opencode/core-routes.js | 1 + .../lib/opencode/feature-routes-runtime.js | 6 + 21 files changed, 1415 insertions(+), 279 deletions(-) create mode 100644 packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx create mode 100644 packages/ui/src/components/sections/magic-prompts/MagicPromptsSidebar.tsx create mode 100644 packages/ui/src/lib/magicPrompts.ts create mode 100644 packages/ui/src/stores/useMagicPromptsStore.ts create mode 100644 packages/web/server/lib/magic-prompts/routes.js create mode 100644 packages/web/server/lib/magic-prompts/runtime.js diff --git a/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx b/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx new file mode 100644 index 00000000..8ca149d9 --- /dev/null +++ b/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx @@ -0,0 +1,322 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { GridLoader } from '@/components/ui/grid-loader'; +import { toast } from '@/components/ui'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { RiInformationLine } from '@remixicon/react'; +import { + fetchMagicPromptOverrides, + getDefaultMagicPromptTemplate, + getMagicPromptDefinition, + resetAllMagicPromptOverrides, + resetMagicPromptOverride, + saveMagicPromptOverride, + type MagicPromptId, +} from '@/lib/magicPrompts'; +import { useMagicPromptsStore } from '@/stores/useMagicPromptsStore'; + +type PromptBlock = { + id: MagicPromptId; + title: string; +}; + +type PromptPageConfig = { + title: string; + description: string; + blocks: PromptBlock[]; +}; + +const PROMPT_PAGE_MAP: Record = { + 'git.commit.generate': { + title: 'Commit Generation', + description: 'Prompts used for commit message generation: visible user message + hidden instructions.', + blocks: [ + { id: 'git.commit.generate.visible', title: 'Visible Prompt' }, + { id: 'git.commit.generate.instructions', title: 'Instructions' }, + ], + }, + 'git.pr.generate': { + title: 'PR Generation', + description: 'Prompts used for PR title/body generation: visible user message + hidden instructions.', + blocks: [ + { id: 'git.pr.generate.visible', title: 'Visible Prompt' }, + { id: 'git.pr.generate.instructions', title: 'Instructions' }, + ], + }, + 'github.pr.review': { + title: 'PR Review', + description: 'Prompts used for PR review flow: visible user message + hidden instruction payload.', + blocks: [ + { id: 'github.pr.review.visible', title: 'Visible Prompt' }, + { id: 'github.pr.review.instructions', title: 'Instructions' }, + ], + }, + 'github.issue.review': { + title: 'Issue Review', + description: 'Prompts used for issue review flow: visible user message + hidden instruction payload.', + blocks: [ + { id: 'github.issue.review.visible', title: 'Visible Prompt' }, + { id: 'github.issue.review.instructions', title: 'Instructions' }, + ], + }, + 'github.pr.checks.review': { + title: 'PR Failed Checks Review', + description: 'Prompts used for PR failed checks analysis.', + blocks: [ + { id: 'github.pr.checks.review.visible', title: 'Visible Prompt' }, + { id: 'github.pr.checks.review.instructions', title: 'Instructions' }, + ], + }, + 'github.pr.comments.review': { + title: 'PR Comments Review', + description: 'Prompts used for PR comments analysis.', + blocks: [ + { id: 'github.pr.comments.review.visible', title: 'Visible Prompt' }, + { id: 'github.pr.comments.review.instructions', title: 'Instructions' }, + ], + }, + 'github.pr.comment.single': { + title: 'Single PR Comment Review', + description: 'Prompts used for single PR comment analysis.', + blocks: [ + { id: 'github.pr.comment.single.visible', title: 'Visible Prompt' }, + { id: 'github.pr.comment.single.instructions', title: 'Instructions' }, + ], + }, + 'git.conflict.resolve': { + title: 'Merge/Rebase Conflict Resolution', + description: 'Prompts used when resolving merge/rebase conflicts with AI.', + blocks: [ + { id: 'git.conflict.resolve.visible', title: 'Visible Prompt' }, + { id: 'git.conflict.resolve.instructions', title: 'Instructions' }, + ], + }, + 'git.integrate.cherrypick.resolve': { + title: 'Cherry-pick Conflict Resolution', + description: 'Prompts used when resolving cherry-pick conflicts in integrate flow.', + blocks: [ + { id: 'git.integrate.cherrypick.resolve.visible', title: 'Visible Prompt' }, + { id: 'git.integrate.cherrypick.resolve.instructions', title: 'Instructions' }, + ], + }, +}; + +const hasOwn = (input: Record, key: string) => Object.prototype.hasOwnProperty.call(input, key); +const isVisiblePromptId = (id: MagicPromptId): boolean => id.endsWith('.visible'); + +export const MagicPromptsPage: React.FC = () => { + const selectedPromptId = useMagicPromptsStore((state) => state.selectedPromptId); + const [loading, setLoading] = React.useState(true); + const [overrides, setOverrides] = React.useState>({}); + const [drafts, setDrafts] = React.useState>({}); + const [savingIds, setSavingIds] = React.useState>({}); + const [resettingIds, setResettingIds] = React.useState>({}); + const [resettingAll, setResettingAll] = React.useState(false); + + React.useEffect(() => { + let active = true; + const load = async () => { + setLoading(true); + try { + const nextOverrides = await fetchMagicPromptOverrides(); + if (!active) return; + setOverrides(nextOverrides); + } catch (error) { + console.warn('Failed to load magic prompts:', error); + toast.error('Failed to load Magic Prompts'); + } finally { + if (active) { + setLoading(false); + } + } + }; + void load(); + return () => { + active = false; + }; + }, []); + + const pageConfig = PROMPT_PAGE_MAP[selectedPromptId] ?? PROMPT_PAGE_MAP['git.commit.generate']; + const getBaseline = React.useCallback((id: MagicPromptId) => { + return hasOwn(overrides, id) ? overrides[id] : getDefaultMagicPromptTemplate(id); + }, [overrides]); + + const getDraft = React.useCallback((id: MagicPromptId) => { + return drafts[id] ?? getBaseline(id); + }, [drafts, getBaseline]); + + const setDraft = React.useCallback((id: MagicPromptId, value: string) => { + setDrafts((current) => { + if (current[id] === value) { + return current; + } + return { ...current, [id]: value }; + }); + }, []); + + const savePrompt = React.useCallback(async (id: MagicPromptId) => { + const value = getDraft(id); + if (isVisiblePromptId(id) && value.trim().length === 0) { + toast.error('Visible prompt cannot be empty'); + return; + } + setSavingIds((current) => ({ ...current, [id]: true })); + try { + const payload = value === getDefaultMagicPromptTemplate(id) + ? await resetMagicPromptOverride(id) + : await saveMagicPromptOverride(id, value); + setOverrides(payload.overrides); + toast.success('Magic prompt saved'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + toast.error('Failed to save magic prompt', { description: message }); + } finally { + setSavingIds((current) => ({ ...current, [id]: false })); + } + }, [getDraft]); + + const resetPrompt = React.useCallback(async (id: MagicPromptId) => { + setResettingIds((current) => ({ ...current, [id]: true })); + try { + const payload = await resetMagicPromptOverride(id); + setOverrides(payload.overrides); + setDrafts((current) => ({ + ...current, + [id]: getDefaultMagicPromptTemplate(id), + })); + toast.success('Prompt reset to default'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + toast.error('Failed to reset prompt', { description: message }); + } finally { + setResettingIds((current) => ({ ...current, [id]: false })); + } + }, []); + + const handleResetAll = React.useCallback(async () => { + setResettingAll(true); + try { + const payload = await resetAllMagicPromptOverrides(); + setOverrides(payload.overrides); + setDrafts({}); + toast.success('All prompt overrides reset'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + toast.error('Failed to reset all prompts', { description: message }); + } finally { + setResettingAll(false); + } + }, []); + + if (loading) { + return ( +
+ + Loading Magic Prompts... +
+ ); + } + + return ( +
+
+
+
+
+

{pageConfig.title}

+ + + + + + {pageConfig.description} + + +
+
+ +
+ + {pageConfig.blocks.map((block, index) => { + const definition = getMagicPromptDefinition(block.id); + const baseline = getBaseline(block.id); + const draft = getDraft(block.id); + const isOverridden = hasOwn(overrides, block.id); + const isDirty = draft !== baseline; + const isInvalidEmptyVisiblePrompt = isVisiblePromptId(block.id) && draft.trim().length === 0; + const saving = savingIds[block.id] === true; + const resetting = resettingIds[block.id] === true; + + return ( +
0 ? 'space-y-3 pt-5 border-t border-border' : 'space-y-3'}> +
+
+

{block.title}

+ + + + + + {definition.description} + + +
+ {definition.placeholders && definition.placeholders.length > 0 && ( +
+ Placeholders: {definition.placeholders.map((item) => `{{${item.key}}}`).join(', ')} +
+ )} +
+ +