Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions web/src/components/projects/integration-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,14 +391,22 @@ function GitHubWebhookSection({ projectId }: { projectId: string }) {
// SCM Tab (GitHub)
// ============================================================================

interface SCMTabProject {
repo?: string | null;
baseBranch?: string | null;
branchPrefix?: string | null;
}

function SCMTab({
projectId,
initialProvider,
initialCredentials,
project,
}: {
projectId: string;
initialProvider: string;
initialCredentials: Map<string, number>;
project?: SCMTabProject;
}) {
const queryClient = useQueryClient();

Expand All @@ -408,12 +416,31 @@ function SCMTab({
const [provider] = useState(initialProvider || 'github');
const [credentialMap, setCredentialMap] = useState<Map<string, number>>(initialCredentials);

// Project-level SCM fields
const [repo, setRepo] = useState(project?.repo ?? '');
const [baseBranch, setBaseBranch] = useState(project?.baseBranch ?? 'main');
const [branchPrefix, setBranchPrefix] = useState(project?.branchPrefix ?? 'feature/');

useEffect(() => {
setCredentialMap(initialCredentials);
}, [initialCredentials]);

useEffect(() => {
setRepo(project?.repo ?? '');
setBaseBranch(project?.baseBranch ?? 'main');
setBranchPrefix(project?.branchPrefix ?? 'feature/');
}, [project?.repo, project?.baseBranch, project?.branchPrefix]);

const saveMutation = useMutation({
mutationFn: async () => {
// Save project-level SCM fields
await trpcClient.projects.update.mutate({
id: projectId,
repo: repo || undefined,
baseBranch,
branchPrefix,
});

// Note: triggers are intentionally omitted — they are managed via the Agent Configs tab
const result = await trpcClient.projects.integrations.upsert.mutate({
projectId,
Expand All @@ -435,6 +462,12 @@ function SCMTab({
return result;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: trpc.projects.getById.queryOptions({ id: projectId }).queryKey,
});
queryClient.invalidateQueries({
queryKey: trpc.projects.listFull.queryOptions().queryKey,
});
queryClient.invalidateQueries({
queryKey: trpc.projects.integrations.list.queryOptions({ projectId }).queryKey,
});
Expand All @@ -451,6 +484,42 @@ function SCMTab({

return (
<div className="space-y-6">
{/* Repository Settings */}
<div className="space-y-4">
<Label className="text-sm font-medium">Repository Settings</Label>
<div className="space-y-2">
<Label htmlFor="scm-repo">Repository (optional)</Label>
<Input
id="scm-repo"
value={repo}
onChange={(e) => setRepo(e.target.value)}
placeholder="owner/repo"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="scm-baseBranch">Base Branch</Label>
<Input
id="scm-baseBranch"
value={baseBranch}
onChange={(e) => setBaseBranch(e.target.value)}
placeholder="main"
/>
</div>
<div className="space-y-2">
<Label htmlFor="scm-branchPrefix">Branch Prefix</Label>
<Input
id="scm-branchPrefix"
value={branchPrefix}
onChange={(e) => setBranchPrefix(e.target.value)}
placeholder="feature/"
/>
</div>
</div>
</div>

<hr className="border-border" />

<p className="text-sm text-muted-foreground">
CASCADE uses two separate GitHub bot accounts to prevent feedback loops. The{' '}
<strong>implementer</strong> writes code and creates PRs. The <strong>reviewer</strong>{' '}
Expand Down Expand Up @@ -564,6 +633,7 @@ export function IntegrationForm({ projectId }: { projectId: string }) {
const scmCredsQuery = useQuery(
trpc.projects.integrationCredentials.list.queryOptions({ projectId, category: 'scm' }),
);
const projectQuery = useQuery(trpc.projects.getById.queryOptions({ id: projectId }));
const [activeTab, setActiveTab] = useState<IntegrationCategory>('pm');

if (integrationsQuery.isLoading) {
Expand Down Expand Up @@ -614,6 +684,7 @@ export function IntegrationForm({ projectId }: { projectId: string }) {
projectId={projectId}
initialProvider={scmProvider}
initialCredentials={scmCredMap}
project={projectQuery.data}
/>
)}
</div>
Expand Down
44 changes: 3 additions & 41 deletions web/src/components/projects/project-general-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import { useState } from 'react';
interface Project {
id: string;
name: string;
repo?: string | null;
baseBranch: string | null;
branchPrefix: string | null;
model: string | null;
maxIterations: number | null;
watchdogTimeoutMs: number | null;
Expand All @@ -27,9 +24,6 @@ function numericFieldDefault(value: number | null | undefined): string {
export function ProjectGeneralForm({ project }: { project: Project }) {
const updateMutation = useProjectUpdate(project.id);
const [name, setName] = useState(project.name);
const [repo, setRepo] = useState(project.repo ?? '');
const [baseBranch, setBaseBranch] = useState(project.baseBranch ?? 'main');
const [branchPrefix, setBranchPrefix] = useState(project.branchPrefix ?? 'feature/');
const [watchdogTimeoutMs, setWatchdogTimeoutMs] = useState(
numericFieldDefault(project.watchdogTimeoutMs),
);
Expand All @@ -44,9 +38,6 @@ export function ProjectGeneralForm({ project }: { project: Project }) {
e.preventDefault();
updateMutation.mutate({
name,
repo: repo || undefined,
baseBranch,
branchPrefix,
watchdogTimeoutMs: watchdogTimeoutMs ? Number.parseInt(watchdogTimeoutMs, 10) : null,
progressModel: progressModel || null,
progressIntervalMinutes: progressIntervalMinutes || null,
Expand All @@ -57,38 +48,9 @@ export function ProjectGeneralForm({ project }: { project: Project }) {

return (
<form onSubmit={handleSubmit} className="max-w-2xl space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} required />
</div>
<div className="space-y-2">
<Label htmlFor="repo">Repository (optional)</Label>
<Input
id="repo"
value={repo}
onChange={(e) => setRepo(e.target.value)}
placeholder="owner/repo"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="baseBranch">Base Branch</Label>
<Input
id="baseBranch"
value={baseBranch}
onChange={(e) => setBaseBranch(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="branchPrefix">Branch Prefix</Label>
<Input
id="branchPrefix"
value={branchPrefix}
onChange={(e) => setBranchPrefix(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} required />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
Expand Down
Loading