diff --git a/ui/src/fixtures/issueThreadInteractionFixtures.ts b/ui/src/fixtures/issueThreadInteractionFixtures.ts index 68030c022ae..debf34e7732 100644 --- a/ui/src/fixtures/issueThreadInteractionFixtures.ts +++ b/ui/src/fixtures/issueThreadInteractionFixtures.ts @@ -329,6 +329,34 @@ export const acceptedSuggestedTasksInteraction = createSuggestTasksInteraction({ }, }); +export const partialAcceptedSuggestedTasksInteraction = createSuggestTasksInteraction({ + id: "interaction-suggest-partial-accepted", + status: "accepted", + resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, + resolvedAt: new Date("2026-04-20T14:16:30.000Z"), + updatedAt: new Date("2026-04-20T14:16:30.000Z"), + result: { + version: 1, + createdTasks: [ + { + clientKey: "root-design", + issueId: "issue-created-1", + identifier: "PAP-1713", + title: "Prototype issue-thread interaction cards", + }, + { + clientKey: "child-stories", + issueId: "issue-created-2", + identifier: "PAP-1714", + title: "Add Storybook coverage for acceptance and rejection states", + parentIssueId: "issue-created-1", + parentIdentifier: "PAP-1713", + }, + ], + skippedClientKeys: ["child-mixed-thread", "hidden-follow-up"], + }, +}); + export const rejectedSuggestedTasksInteraction = createSuggestTasksInteraction({ id: "interaction-suggest-rejected", status: "rejected", @@ -342,8 +370,92 @@ export const rejectedSuggestedTasksInteraction = createSuggestTasksInteraction({ }, }); +export const expiredSuggestedTasksInteraction = createSuggestTasksInteraction({ + id: "interaction-suggest-expired", + status: "expired", + resolvedByAgentId: "agent-codex", + resolvedAt: new Date("2026-04-20T14:17:30.000Z"), + updatedAt: new Date("2026-04-20T14:17:30.000Z"), +}); + +export const cancelledSuggestedTasksInteraction = createSuggestTasksInteraction({ + id: "interaction-suggest-cancelled", + status: "cancelled", + resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, + resolvedAt: new Date("2026-04-20T14:17:45.000Z"), + updatedAt: new Date("2026-04-20T14:17:45.000Z"), +}); + +export const failedSuggestedTasksInteraction = createSuggestTasksInteraction({ + id: "interaction-suggest-failed", + status: "failed", + updatedAt: new Date("2026-04-20T14:17:55.000Z"), +}); + export const pendingAskUserQuestionsInteraction = createAskUserQuestionsInteraction({}); +export const everyQuestionTypeAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ + id: "interaction-questions-every-type", + title: "Confirm rollout answers", + summary: + "This fixture keeps single-select, multi-select, optional, long-copy, and other free-text inputs visible in one form.", + payload: { + version: 1, + title: "Rollout choices", + submitLabel: "Submit rollout answers", + questions: [ + { + id: "free-text", + prompt: "What extra context should the implementer carry into the rollout?", + helpText: + "Use the Other field for free-form text. The schema still stores this as a structured question answer.", + selectionMode: "single", + required: true, + options: [ + { + id: "use-other", + label: "I'll write the answer in Other", + description: "Exercises the free-text answer path without adding a separate schema shape.", + }, + ], + }, + { + id: "single-select", + prompt: "Which rollout path should be treated as the default?", + selectionMode: "single", + required: true, + options: [ + { id: "guarded", label: "Guarded rollout", description: "Ship behind the existing flag first." }, + { id: "direct", label: "Direct rollout", description: "Enable the behavior immediately." }, + ], + }, + { + id: "multi-select", + prompt: "Which review signals should be included in the handoff?", + helpText: + "This intentionally uses longer copy so the option rows prove wrapping behavior in the card.", + selectionMode: "multi", + required: true, + options: [ + { id: "storybook", label: "Storybook snapshots", description: "Reference the visual matrix directly." }, + { id: "unit-tests", label: "Focused unit tests", description: "Keep the regression proof close to the component." }, + { id: "accessibility", label: "A11y notes", description: "Flag keyboard and screen-reader concerns while reviewing the card." }, + ], + }, + { + id: "optional", + prompt: "Optional: which follow-up should wait until after this change?", + selectionMode: "multi", + required: false, + options: [ + { id: "animation", label: "Micro-animation polish" }, + { id: "copy-pass", label: "Long-form copy review" }, + ], + }, + ], + }, +}); + export const answeredAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ id: "interaction-questions-answered", status: "answered", @@ -370,6 +482,41 @@ export const answeredAskUserQuestionsInteraction = createAskUserQuestionsInterac }, }); +export const everyQuestionTypeAnsweredAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ + id: "interaction-questions-every-type-answered", + status: "answered", + title: everyQuestionTypeAskUserQuestionsInteraction.title, + summary: everyQuestionTypeAskUserQuestionsInteraction.summary, + resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, + resolvedAt: new Date("2026-04-20T14:24:30.000Z"), + updatedAt: new Date("2026-04-20T14:24:30.000Z"), + payload: everyQuestionTypeAskUserQuestionsInteraction.payload, + result: { + version: 1, + answers: [ + { + questionId: "free-text", + optionIds: [], + otherText: "Keep the rollout reversible until QA signs off on mobile-width cards.", + }, + { + questionId: "single-select", + optionIds: ["guarded"], + }, + { + questionId: "multi-select", + optionIds: ["storybook", "accessibility"], + }, + { + questionId: "optional", + optionIds: [], + }, + ], + summaryMarkdown: + "Use a guarded rollout, include Storybook and a11y notes, and keep the free-text guidance visible.", + }, +}); + export const commentExpiredAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ id: "interaction-questions-expired-comment", status: "expired", @@ -385,6 +532,32 @@ export const commentExpiredAskUserQuestionsInteraction = createAskUserQuestionsI }, }); +export const cancelledAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ + id: "interaction-questions-cancelled", + status: "cancelled", + resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, + resolvedAt: new Date("2026-04-20T14:25:30.000Z"), + updatedAt: new Date("2026-04-20T14:25:30.000Z"), + result: { + version: 1, + answers: [], + cancelled: true, + cancellationReason: "The board answered this in a later synchronous review.", + summaryMarkdown: null, + }, +}); + +export const failedAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ + id: "interaction-questions-failed", + status: "failed", + updatedAt: new Date("2026-04-20T14:25:45.000Z"), + result: { + version: 1, + answers: [], + summaryMarkdown: null, + }, +}); + export const pendingRequestConfirmationInteraction = createRequestConfirmationInteraction({}); export const genericPendingRequestConfirmationInteraction = createRequestConfirmationInteraction({ @@ -540,6 +713,14 @@ export const failedRequestConfirmationInteraction = createRequestConfirmationInt updatedAt: new Date("2026-04-20T14:42:00.000Z"), }); +export const cancelledRequestConfirmationInteraction = createRequestConfirmationInteraction({ + id: "interaction-confirmation-cancelled", + status: "cancelled", + resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, + resolvedAt: new Date("2026-04-20T14:43:00.000Z"), + updatedAt: new Date("2026-04-20T14:43:00.000Z"), +}); + export const pendingRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({}); @@ -642,6 +823,21 @@ export const rejectedRequestCheckboxConfirmationInteraction = }, }); +export const commentExpiredRequestCheckboxConfirmationInteraction = + createRequestCheckboxConfirmationInteraction({ + id: "interaction-checkbox-expired-comment", + status: "expired", + resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, + resolvedAt: new Date("2026-04-20T14:50:30.000Z"), + updatedAt: new Date("2026-04-20T14:50:30.000Z"), + result: { + version: 1, + outcome: "superseded_by_comment", + commentId: "22222222-2222-4222-8222-222222222222", + selectedOptionIds: [], + }, + }); + export const staleTargetRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({ id: "interaction-checkbox-stale", @@ -679,6 +875,22 @@ export const staleTargetRequestCheckboxConfirmationInteraction = }, }); +export const cancelledRequestCheckboxConfirmationInteraction = + createRequestCheckboxConfirmationInteraction({ + id: "interaction-checkbox-cancelled", + status: "cancelled", + resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, + resolvedAt: new Date("2026-04-20T14:51:30.000Z"), + updatedAt: new Date("2026-04-20T14:51:30.000Z"), + }); + +export const failedRequestCheckboxConfirmationInteraction = + createRequestCheckboxConfirmationInteraction({ + id: "interaction-checkbox-failed", + status: "failed", + updatedAt: new Date("2026-04-20T14:51:45.000Z"), + }); + export const issueThreadInteractionComments: IssueChatComment[] = [ createComment({ id: "comment-thread-board", diff --git a/ui/storybook/stories/issue-thread-interactions.stories.tsx b/ui/storybook/stories/issue-thread-interactions.stories.tsx index c65bc78c664..1e631e443c4 100644 --- a/ui/storybook/stories/issue-thread-interactions.stories.tsx +++ b/ui/storybook/stories/issue-thread-interactions.stories.tsx @@ -10,8 +10,20 @@ import { answeredAskUserQuestionsInteraction, acceptedRequestConfirmationInteraction, boundedRequestCheckboxConfirmationInteraction, + cancelledAskUserQuestionsInteraction, + cancelledRequestCheckboxConfirmationInteraction, + cancelledRequestConfirmationInteraction, + cancelledSuggestedTasksInteraction, + commentExpiredAskUserQuestionsInteraction, + commentExpiredRequestCheckboxConfirmationInteraction, commentExpiredRequestConfirmationInteraction, + everyQuestionTypeAnsweredAskUserQuestionsInteraction, + everyQuestionTypeAskUserQuestionsInteraction, + expiredSuggestedTasksInteraction, + failedAskUserQuestionsInteraction, + failedRequestCheckboxConfirmationInteraction, failedRequestConfirmationInteraction, + failedSuggestedTasksInteraction, genericPendingRequestConfirmationInteraction, issueThreadInteractionComments, issueThreadInteractionEvents, @@ -21,6 +33,7 @@ import { manyOptionsRequestCheckboxConfirmationInteraction, mixedIssueThreadInteractions, optionalDeclineRequestConfirmationInteraction, + partialAcceptedSuggestedTasksInteraction, pendingAskUserQuestionsInteraction, pendingRequestCheckboxConfirmationInteraction, pendingRequestConfirmationInteraction, @@ -39,6 +52,7 @@ import type { RequestCheckboxConfirmationInteraction, RequestConfirmationInteraction, SuggestTasksInteraction, + IssueThreadInteraction, } from "@/lib/issue-thread-interactions"; import { storybookAgentMap } from "../fixtures/paperclipData"; @@ -97,6 +111,39 @@ function ScenarioCard({ ); } +function StaticInteractionCard({ + interaction, +}: { + interaction: IssueThreadInteraction; +}) { + return ( + + ); +} + +function MobileScenario({ + title, + interaction, +}: { + title: string; + interaction: IssueThreadInteraction; +}) { + return ( + +
+ + + +
+
+ ); +} + function InteractiveSuggestedTasksCard() { const [interaction, setInteraction] = useState( pendingSuggestedTasksInteraction, @@ -136,6 +183,100 @@ function InteractiveSuggestedTasksCard() { ); } +function AutoClickInteractionCard({ + buttonText, + children, +}: { + buttonText: string; + children: React.ReactNode; +}) { + const ref = useRef(null); + + useEffect(() => { + const button = Array.from(ref.current?.querySelectorAll("button") ?? []) + .find((candidate) => candidate.textContent?.includes(buttonText)); + button?.click(); + }, [buttonText]); + + return
{children}
; +} + +function InFlightSuggestedTasksCard() { + return ( + + new Promise(() => {})} + onRejectInteraction={() => undefined} + /> + + ); +} + +function InFlightAskUserQuestionsCard() { + const readyInteraction: AskUserQuestionsInteraction = { + ...pendingAskUserQuestionsInteraction, + result: { + version: 1, + answers: [ + { + questionId: "collapse-depth", + optionIds: ["visible-root"], + }, + { + questionId: "post-submit-summary", + optionIds: ["answers-inline"], + }, + ], + }, + }; + + return ( + + new Promise(() => {})} + /> + + ); +} + +function InFlightRequestConfirmationCard() { + return ( + + new Promise(() => {})} + onRejectInteraction={() => undefined} + /> + + ); +} + +function InFlightCheckboxConfirmationCard() { + return ( + + new Promise(() => {})} + onRejectInteraction={() => undefined} + /> + + ); +} + function buildAnsweredInteraction( answers: AskUserQuestionsAnswer[], ): AskUserQuestionsInteraction { @@ -314,6 +455,19 @@ export const SuggestedTasksAccepted: Story = { ), }; +export const SuggestedTasksPartiallyAccepted: Story = { + render: () => ( + + + + + + ), +}; + export const SuggestedTasksRejected: Story = { render: () => ( @@ -332,6 +486,45 @@ export const SuggestedTasksRejected: Story = { ), }; +export const SuggestedTasksExpired: Story = { + render: () => ( + + + + + + ), +}; + +export const SuggestedTasksCancelled: Story = { + render: () => ( + + + + + + ), +}; + +export const SuggestedTasksFailed: Story = { + render: () => ( + + + + + + ), +}; + export const AskUserQuestionsPending: Story = { render: () => ( @@ -345,6 +538,26 @@ export const AskUserQuestionsPending: Story = { ), }; +export const AskUserQuestionsEveryQuestionType: Story = { + render: () => ( + + + undefined} + onCancelInteraction={() => undefined} + /> + + + ), +}; + export const AskUserQuestionsAnswered: Story = { render: () => ( @@ -363,6 +576,58 @@ export const AskUserQuestionsAnswered: Story = { ), }; +export const AskUserQuestionsEveryQuestionTypeAnswered: Story = { + render: () => ( + + + + + + ), +}; + +export const AskUserQuestionsExpiredByComment: Story = { + render: () => ( + + + + + + ), +}; + +export const AskUserQuestionsCancelled: Story = { + render: () => ( + + + + + + ), +}; + +export const AskUserQuestionsFailed: Story = { + render: () => ( + + + + + + ), +}; + export const RequestConfirmationPending: Story = { render: () => ( @@ -570,6 +835,19 @@ export const RequestConfirmationFailed: Story = { ), }; +export const RequestConfirmationCancelled: Story = { + render: () => ( + + + + + + ), +}; + export const RequestConfirmationAccepted = RequestConfirmationConfirmed; export const RequestConfirmationRejected = RequestConfirmationDeclinedWithReason; @@ -661,6 +939,19 @@ export const CheckboxConfirmationRejected: Story = { ), }; +export const CheckboxConfirmationExpiredByComment: Story = { + render: () => ( + + + + + + ), +}; + export const CheckboxConfirmationStaleTarget: Story = { render: () => ( @@ -679,6 +970,32 @@ export const CheckboxConfirmationStaleTarget: Story = { ), }; +export const CheckboxConfirmationCancelled: Story = { + render: () => ( + + + + + + ), +}; + +export const CheckboxConfirmationFailed: Story = { + render: () => ( + + + + + + ), +}; + export const CheckboxConfirmationManyOptions: Story = { render: () => ( @@ -696,6 +1013,129 @@ export const CheckboxConfirmationManyOptions: Story = { ), }; +export const InteractionInFlightStates: Story = { + render: () => ( + +
+
+ + + + + + + + + + + + +
+
+
+ ), +}; + +export const SuggestedTasksMobile390: Story = { + render: () => ( + + ), +}; + +export const AskUserQuestionsMobile390: Story = { + render: () => ( + + ), +}; + +export const RequestConfirmationMobile390: Story = { + render: () => ( + + ), +}; + +export const CheckboxConfirmationMobile390: Story = { + render: () => ( + + ), +}; + +export const StateMatrix: Story = { + render: () => ( + +
+
+ +
+ + + + + + +
+
+ +
+ + + + + +
+
+ +
+ + + + + + + +
+
+ +
+ + + + + + + +
+
+
+
+
+ ), +}; + export const ReviewSurface: Story = { render: () => (