import type { LiveRunForIssue } from "../api/heartbeats"; import type { IssueChatComment, IssueChatTranscriptEntry, } from "../lib/issue-chat-messages"; import type { IssueTimelineEvent } from "../lib/issue-timeline-events"; import type { AskUserQuestionsInteraction, RequestCheckboxConfirmationInteraction, RequestConfirmationInteraction, SuggestTasksInteraction, } from "../lib/issue-thread-interactions"; export const issueThreadInteractionFixtureMeta = { companyId: "company-storybook", projectId: "project-board-ui", issueId: "issue-thread-interactions", currentUserId: "user-board", } as const; function createComment(overrides: Partial): IssueChatComment { const createdAt = overrides.createdAt ?? new Date("2026-04-20T14:00:00.000Z"); return { id: "comment-default", companyId: issueThreadInteractionFixtureMeta.companyId, issueId: issueThreadInteractionFixtureMeta.issueId, authorType: overrides.authorAgentId ? "agent" : "user", authorAgentId: null, authorUserId: issueThreadInteractionFixtureMeta.currentUserId, body: "", presentation: null, metadata: null, createdAt, updatedAt: overrides.updatedAt ?? createdAt, ...overrides, }; } function createSuggestTasksInteraction( overrides: Partial, ): SuggestTasksInteraction { return { id: "interaction-suggest-default", companyId: issueThreadInteractionFixtureMeta.companyId, issueId: issueThreadInteractionFixtureMeta.issueId, kind: "suggest_tasks", title: "Suggested issue tree for the first interaction pass", summary: "Draft task creation stays pending until a reviewer accepts it, so the thread can preview structure without mutating the task system.", status: "pending", continuationPolicy: "wake_assignee", createdByAgentId: "agent-codex", createdByUserId: null, resolvedByAgentId: null, resolvedByUserId: null, createdAt: new Date("2026-04-20T14:11:00.000Z"), updatedAt: new Date("2026-04-20T14:11:00.000Z"), resolvedAt: null, payload: { version: 1, defaultParentId: "PAP-1709", tasks: [ { clientKey: "root-design", title: "Prototype issue-thread interaction cards", description: "Build render-only cards that sit in the issue feed and show suggested tasks before anything is persisted.", priority: "high", assigneeAgentId: "agent-codex", billingCode: "ui-research", labels: ["UI", "interaction"], }, { clientKey: "child-stories", parentClientKey: "root-design", title: "Add Storybook coverage for acceptance and rejection states", description: "Cover pending, accepted, rejected, and collapsed-child previews in a fixture-backed story.", priority: "medium", assigneeAgentId: "agent-qa", labels: ["Storybook"], }, { clientKey: "child-mixed-thread", parentClientKey: "root-design", title: "Prototype the mixed thread feed", description: "Show comments, activity, live runs, and interaction cards in one chronological feed.", priority: "medium", assigneeAgentId: "agent-codex", labels: ["Issue thread"], }, { clientKey: "hidden-follow-up", parentClientKey: "child-mixed-thread", title: "Follow-up polish on spacing and answered summaries", description: "Collapse this under the visible task tree so the preview proves the hidden-descendant treatment.", priority: "low", hiddenInPreview: true, }, ], }, result: null, ...overrides, }; } function createAskUserQuestionsInteraction( overrides: Partial, ): AskUserQuestionsInteraction { return { id: "interaction-questions-default", companyId: issueThreadInteractionFixtureMeta.companyId, issueId: issueThreadInteractionFixtureMeta.issueId, kind: "ask_user_questions", title: "Resolve open UX decisions before Phase 1", summary: "This form stays local until the operator submits it, so the responsible only wakes once after the whole answer set is ready.", status: "pending", continuationPolicy: "wake_assignee", createdByAgentId: "agent-codex", createdByUserId: null, resolvedByAgentId: null, resolvedByUserId: null, createdAt: new Date("2026-04-20T14:18:00.000Z"), updatedAt: new Date("2026-04-20T14:18:00.000Z"), resolvedAt: null, payload: { version: 1, title: "Before I wire the persistence layer, which preview behavior do you want?", submitLabel: "Send answers", questions: [ { id: "collapse-depth", prompt: "How aggressive should the suggested-task preview collapse descendant work?", helpText: "We need enough context to review the tree without making the feed feel like a project plan.", selectionMode: "single", required: true, options: [ { id: "visible-root", label: "Only collapse hidden descendants", description: "Keep top-level and visible child tasks expanded.", }, { id: "collapse-all", label: "Collapse all descendants by default", description: "Show only root tasks until the operator expands the tree.", }, ], }, { id: "post-submit-summary", prompt: "What should the answered-state card emphasize after submission?", helpText: "Pick every summary treatment that would help future reviewers.", selectionMode: "multi", required: true, options: [ { id: "answers-inline", label: "Inline answer pills", description: "Keep the exact operator choices visible under each question.", }, { id: "summary-note", label: "Short markdown summary", description: "Add a compact narrative summary at the bottom of the card.", }, { id: "resolver-meta", label: "Resolver metadata", description: "Show who answered and when without opening the raw thread.", }, ], }, ], }, result: null, ...overrides, }; } function createRequestConfirmationInteraction( overrides: Partial, ): RequestConfirmationInteraction { return { id: "interaction-confirmation-default", companyId: issueThreadInteractionFixtureMeta.companyId, issueId: issueThreadInteractionFixtureMeta.issueId, kind: "request_confirmation", title: "Approve the proposed plan", summary: "The responsible is waiting on a direct board decision before continuing from the plan document.", status: "pending", continuationPolicy: "wake_assignee", createdByAgentId: "agent-codex", createdByUserId: null, resolvedByAgentId: null, resolvedByUserId: null, createdAt: new Date("2026-04-20T14:30:00.000Z"), updatedAt: new Date("2026-04-20T14:30:00.000Z"), resolvedAt: null, payload: { version: 1, prompt: "Approve the plan and let the responsible start implementation?", acceptLabel: "Approve plan", rejectLabel: "Request revisions", rejectRequiresReason: true, rejectReasonLabel: "Describe the plan changes needed before approval", detailsMarkdown: "This confirmation watches the `plan` document revision so stale approvals are blocked if the plan changes.", supersedeOnUserComment: true, target: { type: "issue_document", issueId: issueThreadInteractionFixtureMeta.issueId, key: "plan", revisionId: "11111111-1111-4111-8111-111111111111", revisionNumber: 3, }, }, result: null, ...overrides, }; } function createRequestCheckboxConfirmationInteraction( overrides: Partial, ): RequestCheckboxConfirmationInteraction { return { id: "interaction-checkbox-default", companyId: issueThreadInteractionFixtureMeta.companyId, issueId: issueThreadInteractionFixtureMeta.issueId, kind: "request_checkbox_confirmation", title: "Choose the stale drafts to delete", summary: "The agent found several stale draft documents and needs the board to confirm exactly which ones to remove.", status: "pending", continuationPolicy: "wake_assignee", createdByAgentId: "agent-codex", createdByUserId: null, resolvedByAgentId: null, resolvedByUserId: null, createdAt: new Date("2026-04-20T14:46:00.000Z"), updatedAt: new Date("2026-04-20T14:46:00.000Z"), resolvedAt: null, payload: { version: 1, prompt: "Check the draft documents you want me to delete.", detailsMarkdown: "Only the checked items will be deleted. Leave an item unchecked to keep it for now.", options: [ { id: "draft-march-report", label: "Old draft report", description: "Created by QA during the March test pass.", }, { id: "draft-spec-v1", label: "Spec v1 (superseded)", description: "Replaced by the approved v2 specification.", }, { id: "draft-scratch-notes", label: "Scratch notes", description: "Unstructured notes from the kickoff call.", }, { id: "draft-import-sample", label: "Import sample fixture", description: "Temporary fixture used while wiring the importer.", }, ], defaultSelectedOptionIds: [], minSelected: 0, maxSelected: null, acceptLabel: "Delete selected", rejectLabel: "Request changes", rejectRequiresReason: false, }, result: null, ...overrides, }; } export const pendingSuggestedTasksInteraction = createSuggestTasksInteraction({}); export const acceptedSuggestedTasksInteraction = createSuggestTasksInteraction({ id: "interaction-suggest-accepted", status: "accepted", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:16:00.000Z"), updatedAt: new Date("2026-04-20T14:16:00.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", }, { clientKey: "child-mixed-thread", issueId: "issue-created-3", identifier: "PAP-1715", title: "Prototype the mixed thread feed", parentIssueId: "issue-created-1", parentIdentifier: "PAP-1713", }, { clientKey: "hidden-follow-up", issueId: "issue-created-4", identifier: "PAP-1716", title: "Follow-up polish on spacing and answered summaries", parentIssueId: "issue-created-3", parentIdentifier: "PAP-1715", }, ], }, }); export const rejectedSuggestedTasksInteraction = createSuggestTasksInteraction({ id: "interaction-suggest-rejected", status: "rejected", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:17:00.000Z"), updatedAt: new Date("2026-04-20T14:17:00.000Z"), result: { version: 1, rejectionReason: "Keep the first pass tighter. The hidden follow-on work is useful, but the acceptance story should stay focused on one visible root and one visible child.", }, }); export const pendingAskUserQuestionsInteraction = createAskUserQuestionsInteraction({}); export const answeredAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ id: "interaction-questions-answered", status: "answered", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:24:00.000Z"), updatedAt: new Date("2026-04-20T14:24:00.000Z"), result: { version: 1, answers: [ { questionId: "collapse-depth", optionIds: ["visible-root"], }, { questionId: "post-submit-summary", optionIds: ["answers-inline", "summary-note", "resolver-meta"], }, ], summaryMarkdown: [ "- Keep visible child tasks expanded when they are part of the main review path.", "- Preserve inline answer chips and resolver metadata in the answered state.", "- Add a short summary note so future reviewers understand the operator's intent without replaying the form.", ].join("\n"), }, }); export const commentExpiredAskUserQuestionsInteraction = createAskUserQuestionsInteraction({ id: "interaction-questions-expired-comment", status: "expired", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:25:00.000Z"), updatedAt: new Date("2026-04-20T14:25:00.000Z"), result: { version: 1, answers: [], expirationReason: "superseded_by_comment", commentId: "22222222-2222-4222-8222-222222222222", summaryMarkdown: null, }, }); export const pendingRequestConfirmationInteraction = createRequestConfirmationInteraction({}); export const genericPendingRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-generic-pending", title: "Confirm next step", summary: "The responsible needs a lightweight yes or no before continuing.", continuationPolicy: "none", payload: { version: 1, prompt: "Continue with the current approach?", }, }); export const optionalDeclineRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-optional-decline", continuationPolicy: "none", payload: { version: 1, prompt: "Use the smaller implementation path?", acceptLabel: "Confirm", rejectLabel: "Decline", rejectRequiresReason: false, declineReasonPlaceholder: "Optional: tell the agent what you'd change.", }, }); export const disabledDeclineReasonRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-no-decline-reason", continuationPolicy: "none", payload: { version: 1, prompt: "Close this low-risk follow-up as unnecessary?", acceptLabel: "Close it", rejectLabel: "Keep it", allowDeclineReason: false, }, }); export const acceptedRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-accepted", status: "accepted", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:34:00.000Z"), updatedAt: new Date("2026-04-20T14:34:00.000Z"), result: { version: 1, outcome: "accepted", }, }); export const planApprovalAcceptedRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-plan-accepted", status: "accepted", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:34:00.000Z"), updatedAt: new Date("2026-04-20T14:34:00.000Z"), payload: { version: 1, prompt: "Approve the plan and let the responsible start implementation?", acceptLabel: "Approve plan", rejectLabel: "Request changes", rejectRequiresReason: true, declineReasonPlaceholder: "Optional: what would you like revised?", target: { type: "issue_document", issueId: issueThreadInteractionFixtureMeta.issueId, key: "plan", revisionId: "11111111-1111-4111-8111-111111111111", revisionNumber: 4, }, }, result: { version: 1, outcome: "accepted", }, }); export const rejectedRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-rejected", status: "rejected", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:36:00.000Z"), updatedAt: new Date("2026-04-20T14:36:00.000Z"), result: { version: 1, outcome: "rejected", reason: "Split the migration and UI work into separate reviewable steps.", }, }); export const rejectedNoReasonRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-rejected-no-reason", status: "rejected", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:37:00.000Z"), updatedAt: new Date("2026-04-20T14:37:00.000Z"), result: { version: 1, outcome: "rejected", reason: null, }, }); export const commentExpiredRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-expired-comment", status: "expired", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:38:00.000Z"), updatedAt: new Date("2026-04-20T14:38:00.000Z"), result: { version: 1, outcome: "superseded_by_comment", commentId: "22222222-2222-4222-8222-222222222222", }, }); export const staleTargetRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-expired-target", status: "expired", resolvedByAgentId: "agent-codex", resolvedAt: new Date("2026-04-20T14:40:00.000Z"), updatedAt: new Date("2026-04-20T14:40:00.000Z"), payload: { version: 1, prompt: "Approve the plan and let the responsible start implementation?", acceptLabel: "Approve plan", rejectLabel: "Request revisions", rejectRequiresReason: true, target: { type: "issue_document", issueId: issueThreadInteractionFixtureMeta.issueId, key: "plan", revisionId: "44444444-4444-4444-8444-444444444444", revisionNumber: 4, }, }, result: { version: 1, outcome: "stale_target", staleTarget: { type: "issue_document", issueId: issueThreadInteractionFixtureMeta.issueId, key: "plan", revisionId: "11111111-1111-4111-8111-111111111111", revisionNumber: 3, }, }, }); export const failedRequestConfirmationInteraction = createRequestConfirmationInteraction({ id: "interaction-confirmation-failed", status: "failed", updatedAt: new Date("2026-04-20T14:42:00.000Z"), }); export const pendingRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({}); export const boundedRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({ id: "interaction-checkbox-bounded", title: "Pick the regions to deploy first", summary: "Choose between two and three regions for the Phase 1 rollout.", payload: { version: 1, prompt: "Which regions should we deploy to initially?", detailsMarkdown: "Select at least 2 and at most 3 regions.", options: [ { id: "us-west", label: "US West (Oregon)", description: "Lowest latency for our primary user base." }, { id: "us-east", label: "US East (Virginia)", description: "Redundancy and east coast coverage." }, { id: "eu-west", label: "EU West (Ireland)", description: "GDPR compliance and European users." }, { id: "ap-southeast", label: "AP Southeast (Singapore)", description: "Asia-Pacific expansion." }, ], defaultSelectedOptionIds: ["us-west", "us-east"], minSelected: 2, maxSelected: 3, acceptLabel: "Confirm regions", rejectLabel: "Reconsider", rejectRequiresReason: true, rejectReasonLabel: "What should change about the region set?", }, }); export const acceptedRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({ id: "interaction-checkbox-accepted", status: "accepted", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:49:00.000Z"), updatedAt: new Date("2026-04-20T14:49:00.000Z"), result: { version: 1, outcome: "accepted", selectedOptionIds: ["draft-march-report", "draft-spec-v1"], }, }); const manyOptionList = Array.from({ length: 100 }, (_, index) => { const number = index + 1; return { id: `record-${number}`, label: `Customer record #${number}`, description: number % 4 === 0 ? "Flagged as a possible duplicate during the last import." : undefined, }; }); export const manyOptionsRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({ id: "interaction-checkbox-many", title: "Select the customer records to archive", summary: "The cleanup job found 100 stale customer records. Confirm which ones to archive.", payload: { version: 1, prompt: "Check every customer record you want archived.", detailsMarkdown: "The list scrolls. Use Select all / Clear selection to move quickly.", options: manyOptionList, defaultSelectedOptionIds: [], minSelected: 0, maxSelected: null, acceptLabel: "Archive selected", rejectLabel: "Request changes", rejectRequiresReason: false, }, }); export const acceptedManyRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({ id: "interaction-checkbox-many-accepted", status: "accepted", title: "Select the customer records to archive", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:52:00.000Z"), updatedAt: new Date("2026-04-20T14:52:00.000Z"), payload: manyOptionsRequestCheckboxConfirmationInteraction.payload, result: { version: 1, outcome: "accepted", selectedOptionIds: manyOptionList.slice(0, 42).map((option) => option.id), }, }); export const rejectedRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({ id: "interaction-checkbox-rejected", status: "rejected", resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId, resolvedAt: new Date("2026-04-20T14:50:00.000Z"), updatedAt: new Date("2026-04-20T14:50:00.000Z"), result: { version: 1, outcome: "rejected", reason: "Don't delete anything yet — let me confirm with the data owner first.", }, }); export const staleTargetRequestCheckboxConfirmationInteraction = createRequestCheckboxConfirmationInteraction({ id: "interaction-checkbox-stale", status: "expired", resolvedByAgentId: "agent-codex", resolvedAt: new Date("2026-04-20T14:51:00.000Z"), updatedAt: new Date("2026-04-20T14:51:00.000Z"), payload: { version: 1, prompt: "Check the draft documents you want me to delete.", acceptLabel: "Delete selected", rejectLabel: "Request changes", options: [ { id: "draft-march-report", label: "Old draft report" }, { id: "draft-spec-v1", label: "Spec v1 (superseded)" }, ], target: { type: "issue_document", issueId: issueThreadInteractionFixtureMeta.issueId, key: "plan", revisionId: "44444444-4444-4444-8444-444444444444", revisionNumber: 4, }, }, result: { version: 1, outcome: "stale_target", staleTarget: { type: "issue_document", issueId: issueThreadInteractionFixtureMeta.issueId, key: "plan", revisionId: "11111111-1111-4111-8111-111111111111", revisionNumber: 3, }, }, }); export const issueThreadInteractionComments: IssueChatComment[] = [ createComment({ id: "comment-thread-board", body: "Pressure-test first-class issue-thread interactions before we touch persistence. I want to see the cards in the real feed, not in a disconnected mock.", createdAt: new Date("2026-04-20T14:02:00.000Z"), updatedAt: new Date("2026-04-20T14:02:00.000Z"), }), createComment({ id: "comment-thread-agent", authorAgentId: "agent-codex", authorUserId: null, body: "I found the existing issue chat surface and I am adding prototype-only interaction records so the Storybook review can happen before persistence work.", createdAt: new Date("2026-04-20T14:09:00.000Z"), updatedAt: new Date("2026-04-20T14:09:00.000Z"), runId: "run-thread-interaction", runAgentId: "agent-codex", }), ]; export const issueThreadInteractionEvents: IssueTimelineEvent[] = [ { id: "event-thread-checkout", createdAt: new Date("2026-04-20T14:01:00.000Z"), actorType: "user", actorId: issueThreadInteractionFixtureMeta.currentUserId, statusChange: { from: "todo", to: "in_progress", }, }, ]; export const issueThreadInteractionLiveRuns: LiveRunForIssue[] = [ { id: "run-thread-live", status: "running", invocationSource: "manual", triggerDetail: null, startedAt: "2026-04-20T14:26:00.000Z", finishedAt: null, createdAt: "2026-04-20T14:26:00.000Z", agentId: "agent-codex", agentName: "CodexCoder", adapterType: "codex_local", }, ]; export const issueThreadInteractionTranscriptsByRunId = new Map< string, readonly IssueChatTranscriptEntry[] >([ [ "run-thread-live", [ { kind: "assistant", ts: "2026-04-20T14:26:02.000Z", text: "Wiring the prototype interaction cards into the same issue feed that already renders comments and live runs.", }, { kind: "thinking", ts: "2026-04-20T14:26:04.000Z", text: "Need to keep the payload shapes local to the UI layer so Phase 0 stays non-persistent.", }, ], ], ]); export const mixedIssueThreadInteractions = [ acceptedSuggestedTasksInteraction, pendingRequestConfirmationInteraction, pendingAskUserQuestionsInteraction, ];