fix: improve ci auto-fix delivery (#1560)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useState, type ReactNode } from "react";
|
||||
import { IconEdit, IconInfoCircle, IconRefresh } from "@tabler/icons-react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import {
|
||||
@@ -12,11 +12,13 @@ import {
|
||||
DialogTitle,
|
||||
} from "@kandev/ui/dialog";
|
||||
import { Label } from "@kandev/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover";
|
||||
import { Switch } from "@kandev/ui/switch";
|
||||
import { Textarea } from "@kandev/ui/textarea";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { useTaskCIAutomationOptions } from "@/hooks/domains/github/use-task-ci-options";
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { autoFixRoundForState, findCIAutomationStateForPR } from "@/lib/github/ci-automation";
|
||||
import type { TaskCIAutomationPatch, TaskCIPRAutomationState, TaskPR } from "@/lib/types/github";
|
||||
|
||||
@@ -158,18 +160,23 @@ function CIAutomationRow({
|
||||
checked,
|
||||
disabled,
|
||||
onCheckedChange,
|
||||
help,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
help?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-7 items-center justify-between gap-3 px-1">
|
||||
<Label htmlFor={id} className="min-w-0 flex-1 cursor-pointer truncate text-xs">
|
||||
{label}
|
||||
</Label>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<Label htmlFor={id} className="min-w-0 cursor-pointer truncate text-xs">
|
||||
{label}
|
||||
</Label>
|
||||
{help}
|
||||
</div>
|
||||
<Switch
|
||||
id={id}
|
||||
aria-label={label}
|
||||
@@ -208,7 +215,7 @@ function CIAutomationErrorRow({
|
||||
);
|
||||
}
|
||||
|
||||
function CIAutoFixRoundExplanation({
|
||||
function CIAutoFixRoundHelpButton({
|
||||
state,
|
||||
maxRounds,
|
||||
}: {
|
||||
@@ -216,17 +223,53 @@ function CIAutoFixRoundExplanation({
|
||||
maxRounds: number | null | undefined;
|
||||
}) {
|
||||
const round = autoFixRoundForState(state, maxRounds);
|
||||
return (
|
||||
<div
|
||||
data-testid="ci-auto-fix-round-explanation"
|
||||
className="rounded-md border border-border/70 bg-muted/30 px-2.5 py-2 text-[11px] leading-relaxed text-muted-foreground"
|
||||
const { isFinePointer } = useResponsiveBreakpoint();
|
||||
const [open, setOpen] = useState(false);
|
||||
const trigger = (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-testid="ci-auto-fix-round-help"
|
||||
className="h-5 w-5 cursor-help text-muted-foreground hover:text-foreground"
|
||||
aria-label="Explain auto-fix rounds"
|
||||
>
|
||||
<IconInfoCircle className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
);
|
||||
const explanation = (
|
||||
<span data-testid="ci-auto-fix-round-explanation">
|
||||
Auto-fix has used {round.current} of {round.max} rounds for this PR. A round is counted when
|
||||
Kandev sends or queues a CI auto-fix message. Updating an already queued auto-fix message does
|
||||
not use another round. When this is at {round.max}/{round.max} and there is no pending
|
||||
auto-fix message left to update, Kandev pauses auto-fix for this PR so it cannot loop forever.
|
||||
Disable and re-enable auto-fix after manual review to start over.
|
||||
</div>
|
||||
Kandev sends or queues a CI auto-fix message. Kandev waits for all PR checks to finish before
|
||||
starting a new CI auto-fix turn, so the agent gets the final failed checks and current
|
||||
comments together. Updating an already queued auto-fix message does not use another round.
|
||||
When this is at {round.max}/{round.max} and there is no pending auto-fix message left to
|
||||
update, Kandev pauses auto-fix for this PR so it cannot loop forever. Disable and re-enable
|
||||
auto-fix after manual review to start over.
|
||||
</span>
|
||||
);
|
||||
if (!isFinePointer) {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="top"
|
||||
align="start"
|
||||
portal={false}
|
||||
className="max-w-[280px] text-xs leading-relaxed"
|
||||
>
|
||||
{explanation}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" className="max-w-[280px] text-xs leading-relaxed">
|
||||
{explanation}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,13 +347,15 @@ export function PRCIAutomationControls({ pr }: { pr: TaskPR }) {
|
||||
checked={Boolean(options?.auto_fix_enabled)}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(checked) => patchOption({ auto_fix_enabled: checked })}
|
||||
help={
|
||||
options?.auto_fix_enabled ? (
|
||||
<CIAutoFixRoundHelpButton
|
||||
state={automationState}
|
||||
maxRounds={options.auto_fix_max_rounds}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{options?.auto_fix_enabled && (
|
||||
<CIAutoFixRoundExplanation
|
||||
state={automationState}
|
||||
maxRounds={options.auto_fix_max_rounds}
|
||||
/>
|
||||
)}
|
||||
<CIAutomationRow
|
||||
id={`task-ci-auto-merge-${pr.task_id}`}
|
||||
label="Auto-merge when ready"
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { TaskCIAutomationOptions, TaskPR } from "@/lib/types/github";
|
||||
|
||||
const AUTO_FIX_BADGE_TESTID = "pr-status-auto-fix-chip";
|
||||
const CHIP_TESTID = "pr-status-chip";
|
||||
const ROUND_HELP_TESTID = "ci-auto-fix-round-help";
|
||||
const ROUND_EXPLANATION_TESTID = "ci-auto-fix-round-explanation";
|
||||
|
||||
const testConstants = vi.hoisted(() => ({
|
||||
@@ -187,12 +188,21 @@ describe("PRStatusChip auto-fix round display", () => {
|
||||
expect(screen.getByTestId(AUTO_FIX_BADGE_TESTID).textContent).toBe("Auto-fix 3/12");
|
||||
});
|
||||
|
||||
it("explains the auto-fix round count from the hover popover", async () => {
|
||||
it("explains the auto-fix round count only after hovering the round help icon", async () => {
|
||||
renderWithStore(stateWithAutoFix(1), <PRStatusChip taskId="task-1" />);
|
||||
|
||||
fireEvent.mouseEnter(screen.getByTestId(CHIP_TESTID));
|
||||
const explanation = await screen.findByTestId(ROUND_EXPLANATION_TESTID);
|
||||
expect(screen.queryByTestId(ROUND_EXPLANATION_TESTID)).toBeNull();
|
||||
|
||||
const roundHelp = await screen.findByTestId(ROUND_HELP_TESTID);
|
||||
fireEvent.pointerMove(roundHelp, { pointerType: "mouse" });
|
||||
fireEvent.pointerEnter(roundHelp, { pointerType: "mouse" });
|
||||
fireEvent.mouseMove(roundHelp);
|
||||
const [explanation] = await screen.findAllByTestId(ROUND_EXPLANATION_TESTID);
|
||||
expect(explanation.textContent).toContain("Auto-fix has used 1 of 10 rounds");
|
||||
expect(explanation.textContent).toContain(
|
||||
"Kandev waits for all PR checks to finish before starting a new CI auto-fix turn",
|
||||
);
|
||||
expect(explanation.textContent).toContain(
|
||||
"Updating an already queued auto-fix message does not use another round",
|
||||
);
|
||||
@@ -201,7 +211,7 @@ describe("PRStatusChip auto-fix round display", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the auto-fix round explanation in the mobile drawer", () => {
|
||||
it("opens the auto-fix round explanation from the mobile drawer help icon", async () => {
|
||||
responsiveMock.breakpoint = "mobile";
|
||||
responsiveMock.isFinePointer = false;
|
||||
renderWithStore(stateWithAutoFix(2), <PRStatusChip taskId="task-1" />);
|
||||
@@ -210,7 +220,13 @@ describe("PRStatusChip auto-fix round display", () => {
|
||||
fireEvent.click(screen.getByTestId(CHIP_TESTID));
|
||||
});
|
||||
|
||||
const explanation = screen.getByTestId(ROUND_EXPLANATION_TESTID);
|
||||
expect(screen.queryByTestId(ROUND_EXPLANATION_TESTID)).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId(ROUND_HELP_TESTID));
|
||||
const explanation = await screen.findByTestId(ROUND_EXPLANATION_TESTID);
|
||||
expect(explanation.textContent).toContain("Auto-fix has used 2 of 10 rounds");
|
||||
expect(explanation.textContent).toContain(
|
||||
"Kandev waits for all PR checks to finish before starting a new CI auto-fix turn",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user