diff --git a/packages/dashboard/src/components/settings/WorkspaceSettingsPanel.tsx b/packages/dashboard/src/components/settings/WorkspaceSettingsPanel.tsx index e56518d..619a773 100644 --- a/packages/dashboard/src/components/settings/WorkspaceSettingsPanel.tsx +++ b/packages/dashboard/src/components/settings/WorkspaceSettingsPanel.tsx @@ -144,7 +144,7 @@ export function WorkspaceSettingsPanel({ const [availableRepos, setAvailableRepos] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [activeSection, setActiveSection] = useState<'general' | 'providers' | 'repos' | 'github-access' | 'domain' | 'danger'>('general'); + const [activeSection, setActiveSection] = useState<'general' | 'providers' | 'repos' | 'github-access' | 'automations' | 'domain' | 'danger'>('general'); // Provider connection state const [providerStatus, setProviderStatus] = useState>({}); @@ -178,6 +178,26 @@ export function WorkspaceSettingsPanel({ ttl: number; } | null>(null); + // PR Review config state + const [prReviewConfig, setPrReviewConfig] = useState<{ + enabled: boolean; + reviewers: string[]; + excludeLabels: string[]; + excludeAuthors: string[]; + maxFilesChanged: number; + }>({ + enabled: false, + reviewers: ['claude'], + excludeLabels: ['wip', 'do-not-review'], + excludeAuthors: ['dependabot[bot]'], + maxFilesChanged: 50, + }); + const [prReviewLoading, setPrReviewLoading] = useState(false); + const [prReviewError, setPrReviewError] = useState(null); + const [prReviewSuccess, setPrReviewSuccess] = useState(false); + const [excludeLabelInput, setExcludeLabelInput] = useState(''); + const [excludeAuthorInput, setExcludeAuthorInput] = useState(''); + // Load workspace details useEffect(() => { // Skip loading if workspaceId is invalid (not a UUID) @@ -224,12 +244,90 @@ export function WorkspaceSettingsPanel({ setProviderStatus(connected); } + // Load PR review config + const configResult = await cloudApi.getWorkspaceConfig(workspaceId); + if (configResult.success && configResult.data.prReview) { + setPrReviewConfig(configResult.data.prReview); + } + setIsLoading(false); } loadWorkspace(); }, [workspaceId]); + // Save PR review config + const handleSavePrReviewConfig = useCallback(async () => { + if (!workspace) return; + + setPrReviewLoading(true); + setPrReviewError(null); + setPrReviewSuccess(false); + + const result = await cloudApi.updateWorkspaceConfig(workspace.id, { + prReview: prReviewConfig, + }); + + if (result.success) { + setPrReviewSuccess(true); + setTimeout(() => setPrReviewSuccess(false), 3000); + } else { + setPrReviewError(result.error); + } + + setPrReviewLoading(false); + }, [workspace, prReviewConfig]); + + // Toggle reviewer selection + const toggleReviewer = useCallback((reviewer: string) => { + setPrReviewConfig((prev) => ({ + ...prev, + reviewers: prev.reviewers.includes(reviewer) + ? prev.reviewers.filter((r) => r !== reviewer) + : [...prev.reviewers, reviewer], + })); + }, []); + + // Add exclude label + const addExcludeLabel = useCallback(() => { + const label = excludeLabelInput.trim(); + if (label && !prReviewConfig.excludeLabels.includes(label)) { + setPrReviewConfig((prev) => ({ + ...prev, + excludeLabels: [...prev.excludeLabels, label], + })); + setExcludeLabelInput(''); + } + }, [excludeLabelInput, prReviewConfig.excludeLabels]); + + // Remove exclude label + const removeExcludeLabel = useCallback((label: string) => { + setPrReviewConfig((prev) => ({ + ...prev, + excludeLabels: prev.excludeLabels.filter((l) => l !== label), + })); + }, []); + + // Add exclude author + const addExcludeAuthor = useCallback(() => { + const author = excludeAuthorInput.trim(); + if (author && !prReviewConfig.excludeAuthors.includes(author)) { + setPrReviewConfig((prev) => ({ + ...prev, + excludeAuthors: [...prev.excludeAuthors, author], + })); + setExcludeAuthorInput(''); + } + }, [excludeAuthorInput, prReviewConfig.excludeAuthors]); + + // Remove exclude author + const removeExcludeAuthor = useCallback((author: string) => { + setPrReviewConfig((prev) => ({ + ...prev, + excludeAuthors: prev.excludeAuthors.filter((a) => a !== author), + })); + }, []); + // Start CLI-based OAuth flow for a provider // This just sets state to show the ProviderAuthFlow component, which handles the actual auth const startOAuthFlow = (provider: AIProvider) => { @@ -538,6 +636,7 @@ export function WorkspaceSettingsPanel({ { id: 'general', label: 'General', icon: }, { id: 'providers', label: 'AI Providers', icon: }, { id: 'repos', label: 'Repositories', icon: }, + { id: 'automations', label: 'Automations', icon: }, { id: 'domain', label: 'Domain', icon: }, { id: 'danger', label: 'Danger', icon: }, ]; @@ -966,6 +1065,209 @@ export function WorkspaceSettingsPanel({ )} + {/* Automations Section */} + {activeSection === 'automations' && ( +
+ + + {/* PR Review Automation */} +
+
+
+
+ +
+
+

Automated PR Reviews

+

AI-powered code review for pull requests

+
+
+ setPrReviewConfig((prev) => ({ ...prev, enabled: v }))} + /> +
+ + {prReviewConfig.enabled && ( +
+ {/* Reviewers Selection */} +
+ +

Select which AI agents will review PRs

+
+ {[ + { id: 'claude', label: 'Claude', color: '#D97757' }, + { id: 'codex', label: 'Codex', color: '#10A37F' }, + { id: 'peer', label: 'Peer Review', color: '#7C3AED' }, + ].map((reviewer) => ( + + ))} +
+
+ + {/* Exclude Labels */} +
+ +

PRs with these labels will be skipped

+
+ setExcludeLabelInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && addExcludeLabel()} + placeholder="Add label (e.g., wip, draft)" + className="flex-1 px-4 py-2.5 bg-bg-card border border-border-subtle rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent-cyan focus:ring-1 focus:ring-accent-cyan/30 transition-all" + /> + +
+
+ {prReviewConfig.excludeLabels.map((label) => ( + + {label} + + + ))} +
+
+ + {/* Exclude Authors */} +
+ +

PRs from these authors will be skipped

+
+ setExcludeAuthorInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && addExcludeAuthor()} + placeholder="Add author (e.g., dependabot[bot])" + className="flex-1 px-4 py-2.5 bg-bg-card border border-border-subtle rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent-cyan focus:ring-1 focus:ring-accent-cyan/30 transition-all" + /> + +
+
+ {prReviewConfig.excludeAuthors.map((author) => ( + + {author} + + + ))} +
+
+ + {/* Max Files Changed */} +
+ +

Skip PRs that change more than this many files

+
+ + setPrReviewConfig((prev) => ({ + ...prev, + maxFilesChanged: parseInt(e.target.value, 10), + })) + } + className="flex-1 h-2 bg-bg-hover rounded-full appearance-none cursor-pointer accent-accent-cyan" + /> +
+ + {prReviewConfig.maxFilesChanged} + +
+
+
+
+ )} + + {/* Save Button */} +
+ {prReviewError && ( +
+ + {prReviewError} +
+ )} + {prReviewSuccess && ( +
+ + Settings saved successfully +
+ )} + : } + fullWidth + > + {prReviewLoading ? 'Saving...' : 'Save Automation Settings'} + +
+
+
+ )} + {/* Custom Domain Section */} {activeSection === 'domain' && (
@@ -1366,3 +1668,52 @@ function SyncIcon({ spinning = false }: { spinning?: boolean } = {}) { ); } + +function AutomationIcon() { + return ( + + + + + + + + + + + ); +} + +function PullRequestIcon() { + return ( + + + + + + + ); +} + +function Toggle({ + checked, + onChange, +}: { + checked: boolean; + onChange: (value: boolean) => void; +}) { + return ( + + ); +} diff --git a/packages/dashboard/src/lib/cloudApi.ts b/packages/dashboard/src/lib/cloudApi.ts index b9a3c5a..97f2efc 100644 --- a/packages/dashboard/src/lib/cloudApi.ts +++ b/packages/dashboard/src/lib/cloudApi.ts @@ -890,4 +890,50 @@ export const cloudApi = { method: 'POST', }); }, + + // ===== Workspace Config API ===== + + /** + * Get workspace config (including PR review settings) + */ + async getWorkspaceConfig(workspaceId: string) { + return cloudFetch<{ + prReview?: { + enabled: boolean; + reviewers: string[]; + excludeLabels: string[]; + excludeAuthors: string[]; + maxFilesChanged: number; + }; + }>(`/api/workspaces/${encodeURIComponent(workspaceId)}/config`); + }, + + /** + * Update workspace config (PR review settings, etc.) + */ + async updateWorkspaceConfig(workspaceId: string, config: { + prReview?: { + enabled: boolean; + reviewers: string[]; + excludeLabels: string[]; + excludeAuthors: string[]; + maxFilesChanged: number; + }; + }) { + return cloudFetch<{ + success: boolean; + config: { + prReview?: { + enabled: boolean; + reviewers: string[]; + excludeLabels: string[]; + excludeAuthors: string[]; + maxFilesChanged: number; + }; + }; + }>(`/api/workspaces/${encodeURIComponent(workspaceId)}/config`, { + method: 'PATCH', + body: JSON.stringify(config), + }); + }, };