fix(ui): restore sticky plan confirmation CTAs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
scotttongandPaperclip committed 2026-08-07 16:57:30 -07:00
1 parent 99bd10cb3c
commit 1aaa9eb0c2
1 file changed
+68 -9
@@ -1,10 +1,13 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import type { Issue } from "@paperclipai/shared";
import type { Issue, IssueThreadInteraction } from "@paperclipai/shared";
import { issuesApi } from "@/api/issues";
import { queryKeys } from "@/lib/queryKeys";
import { IssuePlanDecompositionsSection } from "@/components/IssuePlanDecompositionsSection";
import { MarkdownBody } from "@/components/MarkdownBody";
import { DocumentAnnotationsCountChip, IssueDocumentAnnotations } from "@/components/IssueDocumentAnnotations";
import { useIssuePlanDocument } from "@/hooks/useIssuePlanDocument";
import { useLocation } from "@/lib/router";
import { IssuePlanConfirmationActionBar } from "./IssuePlanConfirmationActionBar";
interface IssuePropertiesPlansTabProps {
@@ -14,6 +17,16 @@ interface IssuePropertiesPlansTabProps {
inline?: boolean;
}
function hasPendingPlanConfirmation(interactions: IssueThreadInteraction[] | undefined): boolean {
return (interactions ?? []).some(
(interaction) =>
interaction.kind === "request_confirmation"
&& interaction.status === "pending"
&& interaction.payload.target?.type === "issue_document"
&& interaction.payload.target.key === "plan",
);
}
/**
* Plans tab of the redesigned properties pane (flag: enableTaskChatRedesign).
*
@@ -25,19 +38,42 @@ interface IssuePropertiesPlansTabProps {
*/
export function IssuePropertiesPlansTab({ issue, inline }: IssuePropertiesPlansTabProps) {
const { data: planDocument, isLoading: planDocumentLoading } = useIssuePlanDocument(issue.id);
const location = useLocation();
const [annotationPanelOpen, setAnnotationPanelOpen] = useState(false);
const { data } = useQuery({
queryKey: queryKeys.issues.acceptedPlanDecompositions(issue.id),
queryFn: () => issuesApi.listAcceptedPlanDecompositions(issue.id),
});
const { data: interactions } = useQuery({
queryKey: queryKeys.issues.interactions(issue.id),
queryFn: () => issuesApi.listInteractions(issue.id),
});
const hasPlans = (data?.length ?? 0) > 0;
const pendingPlanConfirmation = hasPendingPlanConfirmation(interactions);
if (!planDocument && !hasPlans) {
return (
<div className="px-1 py-6 text-sm text-muted-foreground">
{planDocumentLoading
? "Loading plan…"
: "No plan yet. The plan document, accepted plans, and their revisions will appear here."}
</div>
<>
{/* This is deliberately outside the plan-document gate: an interaction
can arrive before its plan document query resolves or persists. */}
<IssuePlanConfirmationActionBar issue={issue} inline={inline} />
<div className="px-1 py-6 text-sm text-muted-foreground">
{planDocumentLoading ? (
"Loading plan…"
) : issue.workMode === "planning" ? (
<div className="space-y-2">
<p>This task is in plan mode but no plan document has been written yet.</p>
{pendingPlanConfirmation ? (
<p className="text-amber-foreground">
A plan confirmation is pending, but the plan document it should confirm is missing.
</p>
) : null}
</div>
) : (
"No plan yet. The plan document, accepted plans, and their revisions will appear here."
)}
</div>
</>
);
}
@@ -45,18 +81,41 @@ export function IssuePropertiesPlansTab({ issue, inline }: IssuePropertiesPlansT
<div className="space-y-4 py-2">
{/* Pending plan confirmation: its CTAs pin to the pane's footer slot so
they stay visible while the plan scrolls. */}
{planDocument ? <IssuePlanConfirmationActionBar issue={issue} inline={inline} /> : null}
<IssuePlanConfirmationActionBar issue={issue} inline={inline} />
{planDocument ? (
<section data-testid="issue-plan-document" className="space-y-2">
<div className="text-xs text-muted-foreground">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
{`Revision ${planDocument.latestRevisionNumber ?? 1} · updated ${new Date(planDocument.updatedAt).toLocaleString([], {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
})}`}
<DocumentAnnotationsCountChip
issueId={issue.id}
docKey="plan"
panelOpen={annotationPanelOpen}
onToggle={() => setAnnotationPanelOpen((open) => !open)}
/>
</div>
<MarkdownBody>{planDocument.body}</MarkdownBody>
<IssueDocumentAnnotations
issueId={issue.id}
doc={{
key: "plan",
latestRevisionId: planDocument.latestRevisionId,
latestRevisionNumber: planDocument.latestRevisionNumber,
}}
bodyMarkdown={planDocument.body}
draftDirty={false}
draftConflicted={false}
historicalPreview={false}
locationHash={location.hash}
panelOpen={annotationPanelOpen}
onPanelOpenChange={setAnnotationPanelOpen}
panelPlacement="inline"
>
<MarkdownBody>{planDocument.body}</MarkdownBody>
</IssueDocumentAnnotations>
</section>
) : null}
{hasPlans ? (