diff --git a/ui/src/components/IssueThreadInteractionCard.test.tsx b/ui/src/components/IssueThreadInteractionCard.test.tsx index 5aebdc8c3a3..b4163d12ac4 100644 --- a/ui/src/components/IssueThreadInteractionCard.test.tsx +++ b/ui/src/components/IssueThreadInteractionCard.test.tsx @@ -390,13 +390,17 @@ describe("IssueThreadInteractionCard", () => { ); }); - it("renders a plan confirmation as a distinct state-coloured plan card", () => { + it("renders a plan confirmation as a default-expanded interaction row", () => { const pending = renderCard({ interaction: pendingRequestConfirmationInteraction }); const pendingShell = pending.firstElementChild as HTMLElement; - expect(pendingShell.className).toContain("border-violet-500/80"); + expect(pendingShell.className).toContain("rounded-md"); + expect(pendingShell.className).toContain("border-border"); + expect(pendingShell.className).toContain("bg-card"); + expect(pendingShell.className).not.toContain("border-violet-500/80"); expect(pendingShell.className).not.toContain("border-l-"); - expect(pending.textContent).toContain("Plan"); - expect(pending.textContent).toContain("In review"); + expect(pending.textContent).toContain("Needs approval"); + const toggle = pending.querySelector('button[aria-expanded="true"]'); + expect(toggle).toBeTruthy(); // Approve is a neutral CTA (foreground/background), not the blue primary. const approve = Array.from(pending.querySelectorAll("button")).find((button) => button.textContent?.includes("Approve plan"), @@ -411,7 +415,7 @@ describe("IssueThreadInteractionCard", () => { const accepted = renderCard({ interaction: { ...pendingRequestConfirmationInteraction, status: "accepted" }, }); - expect((accepted.firstElementChild as HTMLElement).className).toContain("border-green-500/80"); + expect((accepted.firstElementChild as HTMLElement).className).toContain("bg-card"); expect(accepted.textContent).toContain("Approved"); act(() => root?.unmount()); @@ -425,7 +429,7 @@ describe("IssueThreadInteractionCard", () => { result: { version: 1, outcome: "rejected", reason: "Tighten the spacing" }, }, }); - expect((rejected.firstElementChild as HTMLElement).className).toContain("border-red-500/80"); + expect((rejected.firstElementChild as HTMLElement).className).toContain("bg-card"); expect(rejected.textContent).toContain("Changes requested"); }); diff --git a/ui/src/components/IssueThreadInteractionCard.tsx b/ui/src/components/IssueThreadInteractionCard.tsx index 22adf75344e..ad02462c7df 100644 --- a/ui/src/components/IssueThreadInteractionCard.tsx +++ b/ui/src/components/IssueThreadInteractionCard.tsx @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import type { Agent } from "@paperclipai/shared"; -import { AlertTriangle, CheckCircle2, ChevronRight, CircleDashed, FileText, GitBranch, ImagePlus, ListChecks, Loader2, MessageSquareQuote, X, XCircle } from "lucide-react"; +import { AlertTriangle, Check, CheckCircle2, ChevronDown, ChevronRight, CircleDashed, FileText, GitBranch, ImagePlus, ListChecks, Loader2, MessageSquareQuote, X, XCircle } from "lucide-react"; import { Link } from "@/lib/router"; import { formatAssigneeUserLabel } from "../lib/assignees"; import { @@ -20,13 +20,12 @@ import { type SuggestedTaskDraft, type SuggestedTaskTreeNode, } from "../lib/issue-thread-interactions"; -import { cn, formatDateTime, formatShortDate } from "../lib/utils"; +import { cn, formatShortDate } from "../lib/utils"; import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody"; import { Button } from "./ui/button"; import { Checkbox } from "./ui/checkbox"; import { PriorityIcon } from "./PriorityIcon"; import { Textarea } from "./ui/textarea"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; const OTHER_ANSWER_ID = "__paperclip_other__"; @@ -99,19 +98,17 @@ function statusLabel(status: IssueThreadInteraction["status"]) { } } -function interactionKindLabel(kind: IssueThreadInteraction["kind"]) { - switch (kind) { - case "suggest_tasks": - return "Suggested tasks"; - case "ask_user_questions": - return "Ask user questions"; - case "request_confirmation": - return "Confirmation"; - case "request_checkbox_confirmation": - return "Checkbox confirmation"; - default: - return kind; - } +function interactionFallbackTitle(interaction: IssueThreadInteraction, isPlan: boolean) { + return interaction.title + ?? (interaction.kind === "suggest_tasks" + ? "Suggested task tree" + : interaction.kind === "ask_user_questions" + ? interaction.payload.title ?? "Questions for the operator" + : interaction.kind === "request_checkbox_confirmation" + ? "Checkbox confirmation requested" + : isPlan + ? "Plan review" + : "Confirmation requested"); } function statusIcon(status: IssueThreadInteraction["status"]) { @@ -130,31 +127,59 @@ function statusIcon(status: IssueThreadInteraction["status"]) { } } -function statusClasses(status: IssueThreadInteraction["status"]) { +function interactionTypeIcon(kind: IssueThreadInteraction["kind"]) { + switch (kind) { + case "suggest_tasks": + return ListChecks; + case "ask_user_questions": + return MessageSquareQuote; + case "request_confirmation": + return FileText; + case "request_checkbox_confirmation": + return ListChecks; + default: + return CircleDashed; + } +} + +function statusBadgeLabel(interaction: IssueThreadInteraction, isPlan: boolean) { + if (interaction.status === "pending") { + switch (interaction.kind) { + case "suggest_tasks": + return "Needs review"; + case "ask_user_questions": + return "Needs answers"; + case "request_checkbox_confirmation": + return "Needs selection"; + case "request_confirmation": + return isPlan ? "Needs approval" : "Needs confirmation"; + default: + return "Pending"; + } + } + + if (isPlan && interaction.status === "accepted") return "Approved"; + if (isPlan && interaction.status === "rejected") return "Changes requested"; + if (interaction.kind === "request_confirmation" && interaction.status === "accepted") return "Confirmed"; + if (interaction.kind === "request_checkbox_confirmation" && interaction.status === "accepted") return "Confirmed"; + return statusLabel(interaction.status); +} + +function statusTone(status: IssueThreadInteraction["status"]): "pending" | "success" | "danger" | "warning" | "muted" { switch (status) { case "accepted": case "answered": - return { - shell: "border-emerald-400/70 bg-transparent", - badge: "border-emerald-500/60 bg-emerald-500/10 text-emerald-900 dark:bg-emerald-500/15 dark:text-emerald-100", - }; + return "success"; case "rejected": case "cancelled": - return { - shell: "border-rose-400/70 bg-transparent", - badge: "border-rose-500/60 bg-rose-500/10 text-rose-900 dark:bg-rose-500/15 dark:text-rose-100", - }; + return "danger"; case "failed": case "expired": - return { - shell: "border-amber-400/70 bg-transparent", - badge: "border-amber-500/60 bg-amber-500/10 text-amber-900 dark:bg-amber-500/15 dark:text-amber-100", - }; + return "warning"; + case "pending": + return "pending"; default: - return { - shell: "border-sky-500/70 bg-transparent", - badge: "border-sky-500/70 bg-sky-500/10 text-sky-900 dark:bg-sky-500/15 dark:text-sky-100", - }; + return "muted"; } } @@ -170,40 +195,59 @@ function isPlanConfirmation(interaction: IssueThreadInteraction): boolean { return target?.type === "issue_document" && target?.key === "plan"; } -function planStatusClasses(status: IssueThreadInteraction["status"]) { - switch (status) { - case "accepted": - case "answered": - return { - shell: "border-2 border-green-500/80 bg-transparent", - badge: "border-green-500/60 bg-green-500/10 text-green-900 dark:bg-green-500/15 dark:text-green-100", - label: "Approved", - Icon: CheckCircle2, - }; - case "rejected": - case "cancelled": - return { - shell: "border-2 border-red-500/80 bg-transparent", - badge: "border-red-500/60 bg-red-500/10 text-red-900 dark:bg-red-500/15 dark:text-red-100", - label: "Changes requested", - Icon: XCircle, - }; - case "failed": - case "expired": - return { - shell: "border-2 border-amber-500/70 bg-transparent", - badge: "border-amber-500/60 bg-amber-500/10 text-amber-900 dark:bg-amber-500/15 dark:text-amber-100", - label: "Expired", - Icon: AlertTriangle, - }; - default: - return { - shell: "border-2 border-violet-500/80 bg-transparent", - badge: "border-violet-500/60 bg-violet-500/10 text-violet-900 dark:bg-violet-500/15 dark:text-violet-100", - label: "In review", - Icon: FileText, - }; - } +function StatusBadge({ + status, + children, +}: { + status: IssueThreadInteraction["status"]; + children: ReactNode; +}) { + const tone = statusTone(status); + return ( + + {children} + + ); +} + +function InteractionOutcomeLine({ + interaction, + resolvedByLabel, +}: { + interaction: IssueThreadInteraction; + resolvedByLabel: string | null; +}) { + if (!resolvedByLabel && !interaction.resolvedAt) return null; + + const Icon = statusIcon(interaction.status); + const tone = statusTone(interaction.status); + return ( +

+ + + {statusBadgeLabel(interaction, isPlanConfirmation(interaction))} + {resolvedByLabel ? ` by ${resolvedByLabel}` : ""} + {interaction.resolvedAt ? ` on ${formatShortDate(interaction.resolvedAt)}` : ""} + +

+ ); } function TaskField({ @@ -583,7 +627,7 @@ function SuggestTasksCard({
{selectedCount < totalTasks ? ( ); } @@ -840,28 +881,22 @@ function AskUserQuestionsCard({ } return ( -
-
- - - Ask user questions - - - {questions.length === 1 - ? "1 question" - : `${questions.length} questions`} - +
+
+ {questions.length === 1 + ? "1 question" + : `${questions.length} questions`}
{interaction.status === "pending" ? ( -
+
{questions.map((question, index) => (
-
+
Question {index + 1}
@@ -884,9 +919,9 @@ function AskUserQuestionsCard({ />
-
+
@@ -935,14 +970,14 @@ function AskUserQuestionsCard({
))} -
+
Submit once after you finish the full form.
{onCancelInteraction ? ( ) : null}
) : interaction.status === "cancelled" ? ( -
+
Question cancelled
{interaction.result?.cancellationReason ? (

{interaction.result.cancellationReason}

@@ -984,7 +1019,7 @@ function AskUserQuestionsCard({ )}
) : interaction.status === "expired" ? ( -
+
{questions.length === 1 ? "Question expired by comment" : "Questions expired by comment"} @@ -1011,7 +1046,7 @@ function AskUserQuestionsCard({ return (
{question.prompt} @@ -1030,7 +1065,7 @@ function AskUserQuestionsCard({ })} {interaction.result?.summaryMarkdown ? ( -
+
Submitted summary
@@ -1156,7 +1191,7 @@ function RequestConfirmationResolution({ : "The requested target changed before this confirmation was resolved."}

{expiredByComment && interaction.result?.commentId ? ( - ) : null} @@ -1300,7 +1335,7 @@ function RequestConfirmationCard({ return (
{interaction.status === "pending" ? ( -
+
{interaction.payload.prompt}
@@ -1320,7 +1355,7 @@ function RequestConfirmationCard({
+ + {title} + + {statusBadgeLabel(interaction, isPlan)} + + + {metaParts.join(" ยท ")} + + - {resolvedByLabel ? ( -
- Resolved by {resolvedByLabel} - {interaction.resolvedAt ? ` on ${formatShortDate(interaction.resolvedAt)}` : ""} + {open ? ( +
+ {interaction.summary ? ( +

{interaction.summary}

+ ) : null} + {interaction.status !== "pending" ? ( + + ) : null} + {interaction.kind === "suggest_tasks" ? ( + + ) : interaction.kind === "ask_user_questions" ? ( + + ) : interaction.kind === "request_checkbox_confirmation" ? ( + + ) : ( + + )}
) : null}