mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-05 01:22:12 +00:00
* feat: auto-review PRs on opened / ready-for-review Trigger Open SWE Review on `pull_request` actions `opened` and `ready_for_review` against the canonical reviewer thread (no need to request open-swe[bot] as a reviewer). `converted_to_draft` now also flips watch=False on the existing reviewer thread. Draft PRs are gated by a tri-state user setting on the profile: inherit team default, always on, or always off. The team-wide `review_draft_prs` setting is the org-wide default; each user can override it in My Settings. External contributors with no Open SWE profile fall back to the team default. * fix: PR review comments — auth source + draft-aware watch toggle - `process_github_pr_ready` now dispatches with `source="github"` so the auth resolver finds the bot token persisted on the thread. The previous `source="github_auto"` fell through to the email-based path in non bot-token-only deployments and failed with a missing-user-email error. - `converted_to_draft` no longer unconditionally clears `watch`. When the PR author's effective `review_draft_prs` setting is on, watch stays on so subsequent pushes still trigger re-reviews while the PR is in draft. * feat(reviewer): skip "no issues found" comment on empty re-reviews A re-review run with no new findings to surface no longer posts another "Open SWE Review: No issues found" comment on the PR. The "no issues" summary now only appears on the first review of a PR — matching Devin's behavior, where subsequent reviews are silent unless there's something new to flag. Resolved-thread reconciliation and ``last_reviewed_sha`` persistence still happen on the skipped path, so findings the user just fixed still get their GitHub threads marked resolved, and the next push event sees an up-to-date dedup SHA. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
import { Navigate, createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
|
|
import { AppShell, SettingsRow, SettingsSection } from "@/components/AppShell";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { api } from "@/lib/api";
|
|
import { buildProfileUpdate, useOptions, useProfile, useSaveProfile } from "@/lib/profile";
|
|
import { useSession } from "@/lib/session";
|
|
|
|
export const Route = createFileRoute("/my-settings")({ component: MySettingsPage });
|
|
|
|
type DraftReviewChoice = "team_default" | "always_on" | "always_off";
|
|
|
|
function toChoice(value: boolean | null | undefined): DraftReviewChoice {
|
|
if (value === true) return "always_on";
|
|
if (value === false) return "always_off";
|
|
return "team_default";
|
|
}
|
|
|
|
function fromChoice(choice: DraftReviewChoice): boolean | null {
|
|
if (choice === "always_on") return true;
|
|
if (choice === "always_off") return false;
|
|
return null;
|
|
}
|
|
|
|
function MySettingsPage() {
|
|
const session = useSession();
|
|
const qc = useQueryClient();
|
|
const navigate = useNavigate();
|
|
const profile = useProfile();
|
|
const options = useOptions();
|
|
const save = useSaveProfile();
|
|
const teamSettings = useQuery({
|
|
queryKey: ["teamSettings"],
|
|
queryFn: api.getTeamSettings,
|
|
enabled: !!session.data,
|
|
});
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
if (session.isLoading) {
|
|
return (
|
|
<main className="p-6">
|
|
<Skeleton className="h-40 w-full" />
|
|
</main>
|
|
);
|
|
}
|
|
if (!session.data) return <Navigate to="/login" />;
|
|
|
|
const handleLogout = async () => {
|
|
await api.logout();
|
|
qc.setQueryData(["session"], null);
|
|
void navigate({ to: "/login" });
|
|
};
|
|
|
|
const firstModel = options.data?.models[0];
|
|
const fallbackModel = firstModel?.id ?? "";
|
|
const fallbackEffort = firstModel?.default_effort ?? "";
|
|
|
|
const draftChoice = toChoice(profile.data?.review_draft_prs);
|
|
const teamDefaultOn = teamSettings.data?.review_draft_prs ?? false;
|
|
const teamDefaultLabel = `Use team default (currently: ${teamDefaultOn ? "On" : "Off"})`;
|
|
|
|
const handleDraftChoiceChange = (next: DraftReviewChoice) => {
|
|
setError(null);
|
|
save
|
|
.mutateAsync(
|
|
buildProfileUpdate(
|
|
profile.data,
|
|
{ review_draft_prs: fromChoice(next) },
|
|
fallbackModel,
|
|
fallbackEffort,
|
|
),
|
|
)
|
|
.catch((e: Error) => setError(e.message));
|
|
};
|
|
|
|
return (
|
|
<AppShell user={session.data} title="My Settings">
|
|
<SettingsSection title="Profile">
|
|
<SettingsRow
|
|
label="Email"
|
|
control={
|
|
<span className="text-xs text-muted-foreground">
|
|
{session.data.email ?? "—"}
|
|
</span>
|
|
}
|
|
/>
|
|
</SettingsSection>
|
|
|
|
<SettingsSection title="Open SWE Review">
|
|
<SettingsRow
|
|
label="Review my draft PRs"
|
|
description="Whether Open SWE Review runs on pull requests you open in draft. When set to the team default, your admin's org-wide setting applies."
|
|
control={
|
|
<Select
|
|
value={draftChoice}
|
|
onValueChange={(v) => handleDraftChoiceChange(v as DraftReviewChoice)}
|
|
disabled={profile.isLoading || save.isPending}
|
|
>
|
|
<SelectTrigger className="w-56">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="team_default">{teamDefaultLabel}</SelectItem>
|
|
<SelectItem value="always_on">Always review my drafts</SelectItem>
|
|
<SelectItem value="always_off">Never review my drafts</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
}
|
|
/>
|
|
</SettingsSection>
|
|
|
|
<SettingsSection title="Account">
|
|
<SettingsRow
|
|
label="Sign out"
|
|
description="End your dashboard session."
|
|
control={
|
|
<Button size="sm" variant="outline" onClick={() => void handleLogout()}>
|
|
Sign out
|
|
</Button>
|
|
}
|
|
/>
|
|
</SettingsSection>
|
|
|
|
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
</AppShell>
|
|
);
|
|
}
|