From d5deb752317508aa47470a20fec4d11a5d2b66b7 Mon Sep 17 00:00:00 2001 From: jeffusion Date: Thu, 26 Mar 2026 12:14:39 +0800 Subject: [PATCH] feat(repo): add project-level review prompt with UI redesign - Add database migration and repository for project review prompts - Add API endpoint for setting project-level prompts - Integrate project prompts into Agent and Codex review flows - Redesign repository management UI with dialog-based prompt editor - Replace flat buttons with Switch for webhook toggle and dedicated prompt button - Add Dialog and DropdownMenu UI components from Radix UI - Add comprehensive tests for wiring and interactions --- frontend/bun.lock | 8 + frontend/package.json | 2 + .../src/components/RepositoryConfigCell.tsx | 141 ++++++++++ frontend/src/components/RepositoryManager.tsx | 10 +- .../src/components/RepositoryTableColumns.tsx | 40 ++- .../src/components/WebhookToggleButton.tsx | 58 ----- frontend/src/components/WebhookToggleCell.tsx | 57 ++++ .../__tests__/RepositoryConfigCell.test.tsx | 77 ++++++ .../__tests__/WebhookToggleCell.test.tsx | 84 ++++++ frontend/src/components/ui/dialog.tsx | 122 +++++++++ frontend/src/components/ui/dropdown-menu.tsx | 200 ++++++++++++++ frontend/src/services/repositoryService.ts | 11 + frontend/tests/visual/fixtures/mockApi.ts | 31 ++- .../__tests__/admin-repositories.test.ts | 86 ++++++ src/controllers/admin.ts | 36 +++ .../repository-review-prompt-repo.test.ts | 67 +++++ src/db/database.ts | 7 +- .../003_repository_review_prompts.ts | 21 ++ .../repository-review-prompt-repo.ts | 94 +++++++ .../__tests__/project-prompt-wiring.test.ts | 244 ++++++++++++++++++ src/review/__tests__/triage-agent.test.ts | 39 +++ src/review/agents/critic-agent.ts | 15 +- src/review/agents/debate-orchestrator.ts | 25 +- src/review/agents/reflexion-agent.ts | 12 +- src/review/agents/specialist-agent.ts | 7 +- src/review/agents/triage-agent.ts | 14 +- src/review/codex/codex-runner.ts | 3 +- src/review/orchestrator.ts | 10 +- src/review/project-review-prompt.ts | 19 ++ src/utils/global-prompt.ts | 22 +- 30 files changed, 1439 insertions(+), 123 deletions(-) create mode 100644 frontend/src/components/RepositoryConfigCell.tsx delete mode 100644 frontend/src/components/WebhookToggleButton.tsx create mode 100644 frontend/src/components/WebhookToggleCell.tsx create mode 100644 frontend/src/components/__tests__/RepositoryConfigCell.test.tsx create mode 100644 frontend/src/components/__tests__/WebhookToggleCell.test.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/dropdown-menu.tsx create mode 100644 src/controllers/__tests__/admin-repositories.test.ts create mode 100644 src/db/__tests__/repository-review-prompt-repo.test.ts create mode 100644 src/db/migrations/003_repository_review_prompts.ts create mode 100644 src/db/repositories/repository-review-prompt-repo.ts create mode 100644 src/review/__tests__/project-prompt-wiring.test.ts create mode 100644 src/review/project-review-prompt.ts diff --git a/frontend/bun.lock b/frontend/bun.lock index affc1b2..7ab3989 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,6 +5,8 @@ "": { "name": "frontend", "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", @@ -216,10 +218,14 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], @@ -228,6 +234,8 @@ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, ""], diff --git a/frontend/package.json b/frontend/package.json index a88914c..c59719f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ "ui:visual:update": "playwright test -c playwright.config.ts --update-snapshots" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", diff --git a/frontend/src/components/RepositoryConfigCell.tsx b/frontend/src/components/RepositoryConfigCell.tsx new file mode 100644 index 0000000..67fac1a --- /dev/null +++ b/frontend/src/components/RepositoryConfigCell.tsx @@ -0,0 +1,141 @@ +"use client" + +import { useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { Loader2, Settings, FileText } from 'lucide-react'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import api from '@/lib/api'; +import type { Repository } from '@/services/repositoryService'; + +interface RepositoryConfigCellProps { + repo: Repository; +} + +export function RepositoryConfigCell({ repo }: RepositoryConfigCellProps) { + const queryClient = useQueryClient(); + const [isPromptDialogOpen, setIsPromptDialogOpen] = useState(false); + const [draftPrompt, setDraftPrompt] = useState(repo.project_review_prompt ?? ''); + + const promptMutation = useMutation({ + mutationFn: async (prompt: string) => { + const { data } = await api.put(`/repositories/${repo.name}/project-prompt`, { + project_review_prompt: prompt, + }); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['repositories'] }); + setIsPromptDialogOpen(false); + toast.success(`已更新 ${repo.name} 的项目级提示词`); + }, + onError: (error: Error) => { + toast.error(`更新失败: ${error.message}`); + }, + }); + + const handleSavePrompt = () => { + promptMutation.mutate(draftPrompt.trim()); + }; + + const handleOpenDialog = () => { + setDraftPrompt(repo.project_review_prompt ?? ''); + setIsPromptDialogOpen(true); + }; + + const hasPrompt = !!repo.project_review_prompt?.trim(); + + return ( + <> + + + + + + 配置项目级提示词 + + 为仓库 {repo.name} 设置审查提示词 + + + +
+ {hasPrompt && ( +
+
+ + 当前配置 +
+

+ {repo.project_review_prompt} +

+
+ )} + +
+ +