1674 lines
54 KiB
TypeScript
1674 lines
54 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import { IssueChatThread } from "@/components/IssueChatThread";
|
|
import { IssueThreadInteractionCard } from "@/components/IssueThreadInteractionCard";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
acceptedManyRequestCheckboxConfirmationInteraction,
|
|
acceptedRequestCheckboxConfirmationInteraction,
|
|
acceptedSuggestedTasksInteraction,
|
|
answeredAskUserQuestionsInteraction,
|
|
acceptedRequestConfirmationInteraction,
|
|
boundedRequestCheckboxConfirmationInteraction,
|
|
commentExpiredRequestConfirmationInteraction,
|
|
declinedToolActionInteraction,
|
|
executedToolActionInteraction,
|
|
expiredToolActionInteraction,
|
|
failedRequestConfirmationInteraction,
|
|
failedToolActionInteraction,
|
|
pendingSecretProposalInteraction,
|
|
executedSecretProposalInteraction,
|
|
failedSecretProposalInteraction,
|
|
rejectedSecretProposalInteraction,
|
|
expiredSecretProposalInteraction,
|
|
genericPendingRequestConfirmationInteraction,
|
|
agentAddressedRequestConfirmationInteraction,
|
|
companyCappedRequestConfirmationInteraction,
|
|
humanOnlyRequestConfirmationInteraction,
|
|
legacyRestrictedRequestConfirmationInteraction,
|
|
notCreatorRequestConfirmationInteraction,
|
|
pendingToolActionDestructiveInteraction,
|
|
pendingToolActionWriteInteraction,
|
|
runningToolActionInteraction,
|
|
issueThreadInteractionComments,
|
|
issueThreadInteractionEvents,
|
|
issueThreadInteractionFixtureMeta,
|
|
issueThreadInteractionLiveRuns,
|
|
issueThreadInteractionTranscriptsByRunId,
|
|
completeRequestItemVerdictsInteraction,
|
|
manyItemsRequestItemVerdictsInteraction,
|
|
manyOptionsRequestCheckboxConfirmationInteraction,
|
|
mixedIssueThreadInteractions,
|
|
optionalDeclineRequestConfirmationInteraction,
|
|
partialRequestItemVerdictsInteraction,
|
|
pendingRequestItemVerdictsInteraction,
|
|
supersededRequestItemVerdictsInteraction,
|
|
pendingAskUserQuestionsInteraction,
|
|
pendingRequestCheckboxConfirmationInteraction,
|
|
pendingRequestConfirmationInteraction,
|
|
pendingSuggestedTasksInteraction,
|
|
planApprovalAcceptedRequestConfirmationInteraction,
|
|
planApprovalResumeFailedRequestConfirmationInteraction,
|
|
rejectedNoReasonRequestConfirmationInteraction,
|
|
rejectedRequestCheckboxConfirmationInteraction,
|
|
rejectedRequestConfirmationInteraction,
|
|
rejectedSuggestedTasksInteraction,
|
|
staleTargetRequestCheckboxConfirmationInteraction,
|
|
staleTargetRequestConfirmationInteraction,
|
|
pendingConnectionIntentInteraction,
|
|
authorizingConnectionIntentInteraction,
|
|
retryConnectionIntentInteraction,
|
|
connectedConnectionIntentInteraction,
|
|
declinedConnectionIntentInteraction,
|
|
supersededConnectionIntentInteraction,
|
|
expiredConnectionIntentInteraction,
|
|
} from "@/fixtures/issueThreadInteractionFixtures";
|
|
import type {
|
|
AskUserQuestionsAnswer,
|
|
AskUserQuestionsInteraction,
|
|
RequestCheckboxConfirmationInteraction,
|
|
RequestConfirmationInteraction,
|
|
RequestItemVerdictsInteraction,
|
|
RequestItemVerdictsResultItem,
|
|
RequestItemVerdictValue,
|
|
SuggestTasksInteraction,
|
|
} from "@/lib/issue-thread-interactions";
|
|
import { storybookAgentMap } from "../fixtures/paperclipData";
|
|
|
|
const boardUserLabels = new Map<string, string>([
|
|
[issueThreadInteractionFixtureMeta.currentUserId, "Riley Board"],
|
|
["user-product", "Mara Product"],
|
|
]);
|
|
|
|
function StoryFrame({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<div className="paperclip-story">
|
|
<main className="paperclip-story__inner space-y-6">{children}</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Section({
|
|
eyebrow,
|
|
title,
|
|
children,
|
|
}: {
|
|
eyebrow: string;
|
|
title: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<section className="paperclip-story__frame overflow-hidden">
|
|
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-border px-5 py-4">
|
|
<div>
|
|
<div className="paperclip-story__label">{eyebrow}</div>
|
|
<h2 className="mt-1 text-xl font-semibold">{title}</h2>
|
|
</div>
|
|
</div>
|
|
<div className="p-5">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ScenarioCard({
|
|
title,
|
|
description,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
description: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<Card className="shadow-none">
|
|
<CardHeader>
|
|
<CardTitle>{title}</CardTitle>
|
|
<CardDescription>{description}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>{children}</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function OpenConnectionIntentDialogStory() {
|
|
const hostRef = useRef<HTMLDivElement>(null);
|
|
useEffect(() => {
|
|
const frame = window.requestAnimationFrame(() => {
|
|
const trigger = Array.from(
|
|
hostRef.current?.querySelectorAll("button") ?? [],
|
|
).find((candidate) =>
|
|
candidate.textContent?.includes("Connect / Use existing"),
|
|
);
|
|
trigger?.click();
|
|
});
|
|
return () => window.cancelAnimationFrame(frame);
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={hostRef}>
|
|
<IssueThreadInteractionCard
|
|
interaction={pendingConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AudienceCard({
|
|
interaction,
|
|
}: {
|
|
interaction: RequestConfirmationInteraction;
|
|
}) {
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={() => undefined}
|
|
onRejectInteraction={() => undefined}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function InteractiveSuggestedTasksCard() {
|
|
const [interaction, setInteraction] = useState<SuggestTasksInteraction>(
|
|
pendingSuggestedTasksInteraction,
|
|
);
|
|
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={(_interaction, selectedClientKeys) =>
|
|
setInteraction({
|
|
...acceptedSuggestedTasksInteraction,
|
|
result: {
|
|
version: 1,
|
|
createdTasks: (
|
|
acceptedSuggestedTasksInteraction.result?.createdTasks ?? []
|
|
).filter(
|
|
(task) => selectedClientKeys?.includes(task.clientKey) ?? true,
|
|
),
|
|
skippedClientKeys: pendingSuggestedTasksInteraction.payload.tasks
|
|
.map((task) => task.clientKey)
|
|
.filter(
|
|
(clientKey) =>
|
|
!(selectedClientKeys?.includes(clientKey) ?? true),
|
|
),
|
|
},
|
|
})
|
|
}
|
|
onRejectInteraction={(_interaction, reason) =>
|
|
setInteraction({
|
|
...rejectedSuggestedTasksInteraction,
|
|
result: {
|
|
version: 1,
|
|
...(rejectedSuggestedTasksInteraction.result ?? {}),
|
|
rejectionReason:
|
|
reason ||
|
|
rejectedSuggestedTasksInteraction.result?.rejectionReason ||
|
|
null,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function buildAnsweredInteraction(
|
|
answers: AskUserQuestionsAnswer[],
|
|
): AskUserQuestionsInteraction {
|
|
const labels = pendingAskUserQuestionsInteraction.payload.questions.flatMap(
|
|
(question) => {
|
|
const answer = answers.find((entry) => entry.questionId === question.id);
|
|
if (!answer) return [];
|
|
return question.options
|
|
.filter((option) => answer.optionIds.includes(option.id))
|
|
.map((option) => option.label);
|
|
},
|
|
);
|
|
|
|
return {
|
|
...answeredAskUserQuestionsInteraction,
|
|
result: {
|
|
version: 1,
|
|
answers,
|
|
summaryMarkdown: labels.map((label) => `- ${label}`).join("\n"),
|
|
},
|
|
};
|
|
}
|
|
|
|
function InteractiveAskUserQuestionsCard() {
|
|
const [interaction, setInteraction] = useState<AskUserQuestionsInteraction>(
|
|
pendingAskUserQuestionsInteraction,
|
|
);
|
|
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onSubmitInteractionAnswers={(_interaction, answers) =>
|
|
setInteraction(buildAnsweredInteraction(answers))
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function InteractiveRequestConfirmationCard() {
|
|
const [interaction, setInteraction] =
|
|
useState<RequestConfirmationInteraction>(
|
|
pendingRequestConfirmationInteraction,
|
|
);
|
|
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={() =>
|
|
setInteraction(acceptedRequestConfirmationInteraction)
|
|
}
|
|
onRejectInteraction={(_interaction, reason) =>
|
|
setInteraction({
|
|
...rejectedRequestConfirmationInteraction,
|
|
result: {
|
|
version: 1,
|
|
outcome: "rejected",
|
|
reason:
|
|
reason ||
|
|
rejectedRequestConfirmationInteraction.result?.reason ||
|
|
null,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function InteractiveRequestCheckboxConfirmationCard({
|
|
pending,
|
|
accepted,
|
|
rejected,
|
|
}: {
|
|
pending: RequestCheckboxConfirmationInteraction;
|
|
accepted: RequestCheckboxConfirmationInteraction;
|
|
rejected: RequestCheckboxConfirmationInteraction;
|
|
}) {
|
|
const [interaction, setInteraction] =
|
|
useState<RequestCheckboxConfirmationInteraction>(pending);
|
|
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={(
|
|
_interaction,
|
|
_selectedClientKeys,
|
|
selectedOptionIds,
|
|
) =>
|
|
setInteraction({
|
|
...accepted,
|
|
payload: pending.payload,
|
|
result: {
|
|
version: 1,
|
|
outcome: "accepted",
|
|
selectedOptionIds: selectedOptionIds ?? [],
|
|
},
|
|
})
|
|
}
|
|
onRejectInteraction={(_interaction, reason) =>
|
|
setInteraction({
|
|
...rejected,
|
|
payload: pending.payload,
|
|
result: {
|
|
version: 1,
|
|
outcome: "rejected",
|
|
reason: reason || rejected.result?.reason || null,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function InteractiveRequestItemVerdictsCard({
|
|
initial = pendingRequestItemVerdictsInteraction,
|
|
}: {
|
|
initial?: RequestItemVerdictsInteraction;
|
|
}) {
|
|
const [interaction, setInteraction] =
|
|
useState<RequestItemVerdictsInteraction>(initial);
|
|
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onSubmitInteractionVerdicts={(_interaction, verdicts) =>
|
|
setInteraction((current) => {
|
|
const existing = current.result?.items ?? [];
|
|
const existingIds = new Set(existing.map((item) => item.id));
|
|
const merged: RequestItemVerdictsResultItem[] = [
|
|
...existing,
|
|
...verdicts
|
|
.filter((verdict) => !existingIds.has(verdict.id))
|
|
.map((verdict) => ({
|
|
id: verdict.id,
|
|
verdict: verdict.verdict as RequestItemVerdictValue,
|
|
reason: verdict.reason ?? null,
|
|
resolvedByUserId:
|
|
issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T15:20:00.000Z"),
|
|
})),
|
|
];
|
|
const complete = merged.length === current.payload.items.length;
|
|
return {
|
|
...current,
|
|
status: complete ? "answered" : "pending",
|
|
resolvedAt: complete ? new Date("2026-04-20T15:20:00.000Z") : null,
|
|
resolvedByUserId: complete
|
|
? issueThreadInteractionFixtureMeta.currentUserId
|
|
: null,
|
|
result: {
|
|
version: 1,
|
|
outcome: "resolved",
|
|
complete,
|
|
items: merged,
|
|
},
|
|
};
|
|
})
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function AutoOpenDeclineRequestConfirmationCard({
|
|
interaction,
|
|
}: {
|
|
interaction: RequestConfirmationInteraction;
|
|
}) {
|
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
const declineButton = Array.from(
|
|
ref.current?.querySelectorAll("button") ?? [],
|
|
).find((button) =>
|
|
button.textContent?.includes(
|
|
interaction.payload.rejectLabel ?? "Decline",
|
|
),
|
|
);
|
|
declineButton?.click();
|
|
}, [interaction]);
|
|
|
|
return (
|
|
<div ref={ref}>
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={() => undefined}
|
|
onRejectInteraction={() => undefined}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Chat & Comments/Issue Thread Interactions",
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"Interaction cards for `suggest_tasks`, `ask_user_questions`, `request_confirmation`, and `request_checkbox_confirmation`, shown both in isolation and inside the real `IssueChatThread` feed.",
|
|
},
|
|
},
|
|
},
|
|
} satisfies Meta;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const SuggestedTasksPending: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending suggested tasks"
|
|
description="Draft issues are selectable before they become real issues."
|
|
>
|
|
<InteractiveSuggestedTasksCard />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const SuggestedTasksAccepted: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Accepted suggested tasks"
|
|
description="Created issues are linked back to their original draft rows."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={acceptedSuggestedTasksInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const SuggestedTasksRejected: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Rejected suggested tasks"
|
|
description="The declined draft stays visible with its rejection note."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={rejectedSuggestedTasksInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const AskUserQuestionsPending: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending question form"
|
|
description="Single- and multi-select questions remain local until submitted."
|
|
>
|
|
<InteractiveAskUserQuestionsCard />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const AskUserQuestionsAnswered: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Answered question form"
|
|
description="Selected answers and the submitted summary remain attached to the thread."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={answeredAskUserQuestionsInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationPending: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending request confirmation"
|
|
description="A generic confirmation can render without a target or custom labels."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={genericPendingRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={() => undefined}
|
|
onRejectInteraction={() => undefined}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationPendingWithTarget: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending request confirmation with target"
|
|
description="The watched plan document renders as a compact target chip."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={pendingRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={() => undefined}
|
|
onRejectInteraction={() => undefined}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationPendingDecliningOptional: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending optional decline"
|
|
description="The decline textarea is visible, but a reason is optional."
|
|
>
|
|
<AutoOpenDeclineRequestConfirmationCard
|
|
interaction={optionalDeclineRequestConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationPendingRequireReason: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending required decline reason"
|
|
description="A plan approval waits for an explicit board decision and requires a decline reason."
|
|
>
|
|
<InteractiveRequestConfirmationCard />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationConfirmed: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Confirmed request confirmation"
|
|
description="The resolved state remains visible without active controls."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={acceptedRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationDeclinedWithReason: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Declined request confirmation"
|
|
description="The decline reason stays attached to the request in the thread."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={rejectedRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationDeclinedNoReason: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Declined without a reason"
|
|
description="The card stays compact when no decline reason was provided."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={rejectedNoReasonRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationExpiredByComment: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Expired by comment"
|
|
description="A board comment superseded the request before resolution."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={commentExpiredRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationExpiredByTargetChange: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Expired by target change"
|
|
description="The watched plan document moved to a newer revision before approval."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={staleTargetRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationPlanApprovalPending: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending plan approval"
|
|
description="The plan-approval variant keeps the approval labels and target chip visible."
|
|
>
|
|
<InteractiveRequestConfirmationCard />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationPlanApprovalConfirmed: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Confirmed plan approval"
|
|
description="The resolved plan approval reads as a compact receipt."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={planApprovalAcceptedRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationPlanApprovalResumeFailed: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Plan approval resume failed"
|
|
description="The approval remains accepted, but the failed continuation is visibly amber and needs attention."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={planApprovalResumeFailedRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationFailed: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Failed request confirmation"
|
|
description="The failed state provides explicit recovery copy."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={failedRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const RequestConfirmationAccepted = RequestConfirmationConfirmed;
|
|
export const RequestConfirmationRejected =
|
|
RequestConfirmationDeclinedWithReason;
|
|
|
|
export const ConnectionIntentStates: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<Section eyebrow="Connection intent" title="Inline setup request states">
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<ScenarioCard
|
|
title="Addressed user"
|
|
description="The responsible user can launch the shared connection setup flow or decline in place."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={pendingConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Other viewer"
|
|
description="Other viewers see who Paperclip is waiting for and receive no connection controls."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={pendingConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId="user-product"
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Authorizing"
|
|
description="The in-flight state prevents duplicate authorization or decline actions."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={authorizingConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Retry needed"
|
|
description="A failed popup or provider callback keeps the intent pending with a safe retry path."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={retryConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Connected"
|
|
description="The terminal card records that the requesting agent receives the connection on continuation."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={connectedConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Declined"
|
|
description="Decline is terminal and wakes the requesting agent without exposing setup controls."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={declinedConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Superseded"
|
|
description="A newer run owns the active request, so the stale card points at the latest one."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={supersededConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Expired"
|
|
description="Closed tasks and expired requests have a quiet terminal state with no authorization controls."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={expiredConnectionIntentInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</div>
|
|
</Section>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ConnectionIntentSetupDialog: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<Section eyebrow="Connection intent" title="Shared setup dialog">
|
|
<OpenConnectionIntentDialogStory />
|
|
</Section>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ConnectionIntentSetupDialogMobile: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<OpenConnectionIntentDialogStory />
|
|
</StoryFrame>
|
|
),
|
|
globals: { viewport: { value: "mobile" } },
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MCP tool-approval card (PAP-13745). A `request_confirmation` carrying a
|
|
// `payload.toolAction` renders as the dedicated tool-approval card: identity
|
|
// header, humanized preview, technical-details drawer, and lifecycle states.
|
|
// The governing rule — approve = run — means no terminal state reads "Accepted".
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function ToolActionCard({
|
|
interaction,
|
|
interactive = false,
|
|
}: {
|
|
interaction: RequestConfirmationInteraction;
|
|
interactive?: boolean;
|
|
}) {
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={interactive ? () => undefined : undefined}
|
|
onRejectInteraction={interactive ? () => undefined : undefined}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const ToolActionPendingWrite: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending · write"
|
|
description="A write tool call awaits approval: identity header, WRITE risk badge, humanized preview, collapsible technical details, expiry countdown, and an Approve & run CTA."
|
|
>
|
|
<ToolActionCard
|
|
interaction={pendingToolActionWriteInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionPendingDestructive: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending · destructive"
|
|
description="A destructive call takes the red risk badge and a destructive primary button; the countdown sits inside the sub-5-minute urgent window."
|
|
>
|
|
<ToolActionCard
|
|
interaction={pendingToolActionDestructiveInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionRunning: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Approved — running…"
|
|
description="The transient post-approve state: an amber spinner strip that self-resolves to Executed or Failed when the gateway writes back. Buttons are removed to prevent double-submit."
|
|
>
|
|
<ToolActionCard interaction={runningToolActionInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionExecuted: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Executed"
|
|
description="The green terminal state reports the real outcome — a result summary plus a deep-link — never a bare checkmark or “Accepted”."
|
|
>
|
|
<ToolActionCard interaction={executedToolActionInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionFailed: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Failed"
|
|
description="You approved it and it ran, but the connector errored: amber card, red icon, and the verbatim connector error. Distinct from Declined."
|
|
>
|
|
<ToolActionCard interaction={failedToolActionInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionDeclined: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Declined"
|
|
description="You rejected it and nothing ran: red card with a dimmed/greyscale identity header and the decline reason attached."
|
|
>
|
|
<ToolActionCard interaction={declinedToolActionInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionExpired: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Expired"
|
|
description="Nobody responded within 60 minutes: a neutral grey (not error-red) card that always states the rule and the recovery path — the agent can request approval again."
|
|
>
|
|
<ToolActionCard interaction={expiredToolActionInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionLegacyGeneric: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Legacy · no toolAction"
|
|
description="A confirmation without a toolAction payload keeps the existing generic rendering unchanged — the tool-approval surface is strictly additive."
|
|
>
|
|
<ToolActionCard
|
|
interaction={genericPendingRequestConfirmationInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
function SecretProposalCard({
|
|
interaction,
|
|
interactive = false,
|
|
}: {
|
|
interaction: RequestConfirmationInteraction;
|
|
interactive?: boolean;
|
|
}) {
|
|
return (
|
|
<IssueThreadInteractionCard
|
|
interaction={interaction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
onAcceptInteraction={interactive ? () => undefined : undefined}
|
|
onRejectInteraction={interactive ? () => undefined : undefined}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const SecretProposalPending: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending secret binding"
|
|
description="A human reviews safe binding metadata, the agent-authored reason, and expiry before approving the real write."
|
|
>
|
|
<SecretProposalCard
|
|
interaction={pendingSecretProposalInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const SecretProposalExecuted: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Secret binding executed"
|
|
description="Acceptance is only shown as successful after the existing proposal transaction creates the binding."
|
|
>
|
|
<SecretProposalCard interaction={executedSecretProposalInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const SecretProposalFailed: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Secret binding failed"
|
|
description="An accepted request that fails execution is unmistakably FAILED and exposes the safe error code."
|
|
>
|
|
<SecretProposalCard interaction={failedSecretProposalInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const SecretProposalRejected: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Secret binding rejected"
|
|
description="The rejection reason remains in the thread and the card states that no binding was created."
|
|
>
|
|
<SecretProposalCard interaction={rejectedSecretProposalInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const SecretProposalExpired: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Secret binding expired"
|
|
description="Expired proposals are neutral, non-actionable receipts that require a fresh proposal."
|
|
>
|
|
<SecretProposalCard interaction={expiredSecretProposalInteraction} />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const SecretProposalAllStates: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<Section eyebrow="Secret binding proposal" title="All lifecycle states">
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<ScenarioCard
|
|
title="1 · Pending"
|
|
description="Safe metadata and approval actions."
|
|
>
|
|
<SecretProposalCard
|
|
interaction={pendingSecretProposalInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="2 · Executed"
|
|
description="The binding was created."
|
|
>
|
|
<SecretProposalCard
|
|
interaction={executedSecretProposalInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="3 · FAILED"
|
|
description="Accepted, then failed closed."
|
|
>
|
|
<SecretProposalCard interaction={failedSecretProposalInteraction} />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="4 · Rejected"
|
|
description="The binding was not created."
|
|
>
|
|
<SecretProposalCard
|
|
interaction={rejectedSecretProposalInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="5 · Expired"
|
|
description="A fresh proposal is required."
|
|
>
|
|
<SecretProposalCard
|
|
interaction={expiredSecretProposalInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</div>
|
|
</Section>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
/**
|
|
* PAP-17280: the audience row every pending card now carries. `Anyone` is the
|
|
* default a requester gets by omitting `resolverPolicy`; the rest are the
|
|
* narrowings a requester, a company cap, or a governed action asks for.
|
|
*/
|
|
export const ResolverAudienceStates: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<Section eyebrow="Resolver audience" title="Who can respond (PAP-17280)">
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<ScenarioCard
|
|
title="Default · Anyone"
|
|
description="No resolverPolicy was requested, so the card is open company attention — the board or any agent, including the one that asked."
|
|
>
|
|
<AudienceCard interaction={pendingRequestConfirmationInteraction} />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Anyone except creator"
|
|
description="Requested on purpose when the answer has to be independent of the agent that asked."
|
|
>
|
|
<AudienceCard
|
|
interaction={notCreatorRequestConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Human only"
|
|
description="Reserved for a person: agents are turned away by the server, and the copy says so."
|
|
>
|
|
<AudienceCard
|
|
interaction={humanOnlyRequestConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Named addressee"
|
|
description="One agent owns the response; the card stays out of the open attention feed."
|
|
>
|
|
<AudienceCard
|
|
interaction={agentAddressedRequestConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Narrowed by a company cap"
|
|
description="The request asked for Anyone; company interaction governance capped the kind, and the card explains the narrowing."
|
|
>
|
|
<AudienceCard
|
|
interaction={companyCappedRequestConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Legacy restricted card"
|
|
description="Created before Anyone became the default. Migration keeps it restricted fail-closed and the card says a new card would be open."
|
|
>
|
|
<AudienceCard
|
|
interaction={legacyRestrictedRequestConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</div>
|
|
</Section>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionAllStates: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<Section
|
|
eyebrow="MCP Tool Approval"
|
|
title="All six lifecycle states (PAP-13745)"
|
|
>
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<ScenarioCard
|
|
title="1 · Pending (write)"
|
|
description="Awaiting approval — Approve & run."
|
|
>
|
|
<ToolActionCard
|
|
interaction={pendingToolActionWriteInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="1b · Pending (destructive)"
|
|
description="Red risk badge, urgent countdown."
|
|
>
|
|
<ToolActionCard
|
|
interaction={pendingToolActionDestructiveInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="2 · Approved — running…"
|
|
description="Transient, self-resolving spinner."
|
|
>
|
|
<ToolActionCard interaction={runningToolActionInteraction} />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="3 · Executed"
|
|
description="Green, with a result summary."
|
|
>
|
|
<ToolActionCard interaction={executedToolActionInteraction} />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="4 · Failed"
|
|
description="Ran, but the connector errored."
|
|
>
|
|
<ToolActionCard interaction={failedToolActionInteraction} />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="5 · Declined"
|
|
description="Rejected — nothing ran."
|
|
>
|
|
<ToolActionCard interaction={declinedToolActionInteraction} />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="6 · Expired"
|
|
description="No response in 60 min."
|
|
>
|
|
<ToolActionCard interaction={expiredToolActionInteraction} />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Legacy · no toolAction"
|
|
description="Unchanged generic rendering."
|
|
>
|
|
<ToolActionCard
|
|
interaction={genericPendingRequestConfirmationInteraction}
|
|
interactive
|
|
/>
|
|
</ScenarioCard>
|
|
</div>
|
|
</Section>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ToolActionMobile: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Mobile · 390"
|
|
description="Single column: risk badge wraps under the tool name, actions stack full-width, the technical drawer stays collapsed."
|
|
>
|
|
<div className="mx-auto max-w-[358px]">
|
|
<ToolActionCard
|
|
interaction={pendingToolActionWriteInteraction}
|
|
interactive
|
|
/>
|
|
</div>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
globals: { viewport: { value: "mobile1" } },
|
|
};
|
|
|
|
export const CheckboxConfirmationPending: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Pending checkbox confirmation"
|
|
description="Board users select any number of options, with frontend-owned select-all and clear controls."
|
|
>
|
|
<InteractiveRequestCheckboxConfirmationCard
|
|
pending={pendingRequestCheckboxConfirmationInteraction}
|
|
accepted={acceptedRequestCheckboxConfirmationInteraction}
|
|
rejected={rejectedRequestCheckboxConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const CheckboxConfirmationBounded: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Min/max constrained selection"
|
|
description="The card enforces minimum and maximum selection counts and requires a decline reason."
|
|
>
|
|
<InteractiveRequestCheckboxConfirmationCard
|
|
pending={boundedRequestCheckboxConfirmationInteraction}
|
|
accepted={acceptedRequestCheckboxConfirmationInteraction}
|
|
rejected={rejectedRequestCheckboxConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const CheckboxConfirmationAccepted: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Accepted checkbox confirmation"
|
|
description="The resolved state leads with a count and lists the selected labels."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={acceptedRequestCheckboxConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const CheckboxConfirmationAcceptedMany: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Accepted large selection"
|
|
description="Large resolved selections summarize by count first and bound the inline chips."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={acceptedManyRequestCheckboxConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const CheckboxConfirmationRejected: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Declined checkbox confirmation"
|
|
description="The decline reason stays attached to the request in the thread."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={rejectedRequestCheckboxConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const CheckboxConfirmationStaleTarget: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Expired by target change"
|
|
description="The watched plan revision moved before the selection was confirmed."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={staleTargetRequestCheckboxConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const CheckboxConfirmationManyOptions: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="Around 100 options"
|
|
description="The list stays compact inside a bounded scroll region even with 100 options."
|
|
>
|
|
<InteractiveRequestCheckboxConfirmationCard
|
|
pending={manyOptionsRequestCheckboxConfirmationInteraction}
|
|
accepted={acceptedManyRequestCheckboxConfirmationInteraction}
|
|
rejected={rejectedRequestCheckboxConfirmationInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ItemVerdictsPending: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="S1 / S2 — draft then apply"
|
|
description="Mark each item Approve or Reject (reject reveals a required reason), then Apply N decisions in one pass. Approve all is the common-case accelerator."
|
|
>
|
|
<InteractiveRequestItemVerdictsCard />
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ItemVerdictsPartial: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="S3 / S4 — partial progress"
|
|
description="Two items already applied (one approved, one rejected with its reason echoed); three remain actionable. The card stays alive and shows 2 of 5 decided."
|
|
>
|
|
<InteractiveRequestItemVerdictsCard
|
|
initial={partialRequestItemVerdictsInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ItemVerdictsComplete: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="S5 — complete"
|
|
description="Every item has a terminal verdict. The summary chip reads 5 decided · 3 approved · 2 rejected and the row leaves the queue."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={completeRequestItemVerdictsInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ItemVerdictsSuperseded: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="S6 — stale / superseded"
|
|
description="A later comment expired the review. Items already applied cannot be reverted; the remaining items were cancelled."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={supersededRequestItemVerdictsInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ItemVerdictsManyItems: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<ScenarioCard
|
|
title="S7 — long list"
|
|
description="24 items decided in passes; the expanded list scrolls in a bounded region and reuses the 200-item cap."
|
|
>
|
|
<InteractiveRequestItemVerdictsCard
|
|
initial={manyItemsRequestItemVerdictsInteraction}
|
|
/>
|
|
</ScenarioCard>
|
|
</StoryFrame>
|
|
),
|
|
};
|
|
|
|
export const ReviewSurface: Story = {
|
|
render: () => (
|
|
<StoryFrame>
|
|
<section className="paperclip-story__frame p-6">
|
|
<div className="paperclip-story__label">Thread interactions</div>
|
|
<div className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
This review surface pressure-tests the thread interaction kinds
|
|
directly inside the issue chat surface. The card language leans closer
|
|
to annotated review sheets than generic admin widgets so the objects
|
|
feel like first-class work artifacts in the thread.
|
|
</div>
|
|
</section>
|
|
|
|
<Section
|
|
eyebrow="Suggested Tasks"
|
|
title="Pending, accepted, and rejected task-tree cards"
|
|
>
|
|
<div className="grid gap-6 xl:grid-cols-3">
|
|
<ScenarioCard
|
|
title="Pending"
|
|
description="The draft tree stays editable and non-persistent until someone accepts or rejects it."
|
|
>
|
|
<InteractiveSuggestedTasksCard />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Accepted"
|
|
description="Accepted state resolves to created issue links while keeping the original suggestion visible in-thread."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={acceptedSuggestedTasksInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Rejected"
|
|
description="The rejection reason remains attached to the artifact so future reviewers can see why the draft was declined."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={rejectedSuggestedTasksInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section
|
|
eyebrow="Ask User Questions"
|
|
title="Pending multi-question form and answered summary"
|
|
>
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<ScenarioCard
|
|
title="Pending"
|
|
description="Answers stay local across the whole form until the operator submits the final response."
|
|
>
|
|
<InteractiveAskUserQuestionsCard />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Answered"
|
|
description="The answered state keeps the exact choices visible and adds a compact summary note for later review."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={answeredAskUserQuestionsInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section
|
|
eyebrow="Request Confirmation"
|
|
title="Plan approval and compact resolution states"
|
|
>
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<ScenarioCard
|
|
title="Plan approval"
|
|
description="The pending card links to the watched plan revision and requires a reason when declined."
|
|
>
|
|
<InteractiveRequestConfirmationCard />
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Accepted"
|
|
description="Accepted confirmations stay visible as resolved work artifacts."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={acceptedRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Rejected"
|
|
description="Rejected confirmations keep the board's decline reason attached."
|
|
>
|
|
<IssueThreadInteractionCard
|
|
interaction={rejectedRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</ScenarioCard>
|
|
<ScenarioCard
|
|
title="Expired states"
|
|
description="Comment and target-change expiry states are compact and disabled."
|
|
>
|
|
<div className="space-y-4">
|
|
<IssueThreadInteractionCard
|
|
interaction={commentExpiredRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
<IssueThreadInteractionCard
|
|
interaction={staleTargetRequestConfirmationInteraction}
|
|
agentMap={storybookAgentMap}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
/>
|
|
</div>
|
|
</ScenarioCard>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section
|
|
eyebrow="Mixed Feed"
|
|
title="Interaction cards in the real issue thread"
|
|
>
|
|
<ScenarioCard
|
|
title="IssueChatThread composition"
|
|
description="Comments, timeline events, accepted task suggestions, a pending confirmation, a pending question form, and an active run share the same feed."
|
|
>
|
|
<div className="overflow-hidden rounded-[32px] border border-border/70 bg-[linear-gradient(135deg,rgba(14,165,233,0.08),transparent_28%),linear-gradient(180deg,rgba(245,158,11,0.08),transparent_42%),var(--background)] p-5 shadow-[0_30px_80px_rgba(15,23,42,0.10)]">
|
|
<IssueChatThread
|
|
comments={issueThreadInteractionComments}
|
|
interactions={mixedIssueThreadInteractions}
|
|
timelineEvents={issueThreadInteractionEvents}
|
|
liveRuns={issueThreadInteractionLiveRuns}
|
|
transcriptsByRunId={issueThreadInteractionTranscriptsByRunId}
|
|
hasOutputForRun={(runId) => runId === "run-thread-live"}
|
|
companyId={issueThreadInteractionFixtureMeta.companyId}
|
|
projectId={issueThreadInteractionFixtureMeta.projectId}
|
|
currentUserId={issueThreadInteractionFixtureMeta.currentUserId}
|
|
userLabelMap={boardUserLabels}
|
|
agentMap={storybookAgentMap}
|
|
onAdd={async () => {}}
|
|
showComposer={false}
|
|
/>
|
|
</div>
|
|
</ScenarioCard>
|
|
</Section>
|
|
</StoryFrame>
|
|
),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
"Covers the prototype states called out in [PAP-1709](/PAP/issues/PAP-1709): suggested-task previews, collapsed descendants, rejection reasons, request confirmations, multi-question answers, and a mixed issue thread.",
|
|
},
|
|
},
|
|
},
|
|
};
|