1625 lines
57 KiB
TypeScript
1625 lines
57 KiB
TypeScript
import { legacyIssueThreadInteractionResolverPolicyAlias } from "@paperclipai/shared";
|
||
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,
|
||
ConnectionIntentInteraction,
|
||
IssueThreadInteractionBase,
|
||
RequestCheckboxConfirmationInteraction,
|
||
RequestConfirmationInteraction,
|
||
RequestConfirmationSecretProposalPayload,
|
||
RequestConfirmationToolActionPayload,
|
||
RequestItemVerdictsInteraction,
|
||
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;
|
||
|
||
/**
|
||
* Resolver-audience snapshot fields shared by every interaction fixture.
|
||
*
|
||
* The default is the open audience: `anyone` with `inherited` provenance, which
|
||
* is what the server returns for a create request that omits `resolverPolicy`
|
||
* (PAP-17277 contract, PAP-17280 surfaces). A fixture that wants a restriction
|
||
* states it explicitly, exactly as a requester must.
|
||
*/
|
||
function resolverAudienceFields(
|
||
overrides: Partial<IssueThreadInteractionBase>,
|
||
): Pick<
|
||
IssueThreadInteractionBase,
|
||
| "resolverPolicy"
|
||
| "requestedResolverPolicy"
|
||
| "effectiveResolverPolicy"
|
||
| "resolverPolicyProvenance"
|
||
| "effectiveResolverPolicySource"
|
||
| "legacyResolverPolicyAliases"
|
||
> {
|
||
const requested = overrides.requestedResolverPolicy ?? overrides.resolverPolicy ?? "anyone";
|
||
const effective = overrides.effectiveResolverPolicy ?? requested;
|
||
return {
|
||
resolverPolicy: requested,
|
||
requestedResolverPolicy: requested,
|
||
effectiveResolverPolicy: effective,
|
||
resolverPolicyProvenance:
|
||
overrides.resolverPolicyProvenance ?? (requested === "anyone" ? "inherited" : "explicit"),
|
||
effectiveResolverPolicySource:
|
||
overrides.effectiveResolverPolicySource ?? (effective === requested ? "requested" : "company_cap"),
|
||
legacyResolverPolicyAliases: overrides.legacyResolverPolicyAliases ?? {
|
||
requested: legacyIssueThreadInteractionResolverPolicyAlias(requested),
|
||
effective: legacyIssueThreadInteractionResolverPolicyAlias(effective),
|
||
},
|
||
};
|
||
}
|
||
|
||
function createComment(overrides: Partial<IssueChatComment>): 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>,
|
||
): 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,
|
||
...resolverAudienceFields(overrides),
|
||
};
|
||
}
|
||
|
||
function createAskUserQuestionsInteraction(
|
||
overrides: Partial<AskUserQuestionsInteraction>,
|
||
): 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,
|
||
...resolverAudienceFields(overrides),
|
||
};
|
||
}
|
||
|
||
function createRequestConfirmationInteraction(
|
||
overrides: Partial<RequestConfirmationInteraction>,
|
||
): 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,
|
||
...resolverAudienceFields(overrides),
|
||
};
|
||
}
|
||
|
||
function createRequestCheckboxConfirmationInteraction(
|
||
overrides: Partial<RequestCheckboxConfirmationInteraction>,
|
||
): 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: "Reject",
|
||
rejectRequiresReason: false,
|
||
},
|
||
result: null,
|
||
...overrides,
|
||
...resolverAudienceFields(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({});
|
||
|
||
/**
|
||
* A pending question whose last option is a first-class free-text choice
|
||
* (`freeText: true`). Selecting it reveals an inline text field instead of
|
||
* acting as a dead radio, and the built-in "Other" link is suppressed
|
||
* (PAP-419).
|
||
*/
|
||
export const pendingAskUserQuestionsWithFreeTextOption = createAskUserQuestionsInteraction({
|
||
id: "interaction-questions-freetext",
|
||
payload: {
|
||
version: 1,
|
||
title: "How should we name the new surface?",
|
||
submitLabel: "Send answers",
|
||
questions: [
|
||
{
|
||
id: "surface-name",
|
||
prompt: "What should we call the new surface?",
|
||
selectionMode: "single",
|
||
required: true,
|
||
options: [
|
||
{
|
||
id: "keep-tasks",
|
||
label: "Keep calling it Tasks",
|
||
},
|
||
{
|
||
id: "rename-work",
|
||
label: "Rename it Work",
|
||
},
|
||
{
|
||
id: "describe-it",
|
||
label: "I'll describe it",
|
||
description: "Tell us the exact name you have in mind.",
|
||
freeText: true,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
});
|
||
|
||
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: "Reject",
|
||
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 planApprovalResumeFailedRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
||
...planApprovalAcceptedRequestConfirmationInteraction,
|
||
id: "interaction-confirmation-plan-resume-failed",
|
||
result: {
|
||
version: 1,
|
||
outcome: "accepted",
|
||
resumeFailure: {
|
||
status: "needs_attention",
|
||
errorCode: "adapter_failed",
|
||
attempt: 3,
|
||
maxAttempts: 3,
|
||
runId: "11111111-1111-4111-8111-222222222222",
|
||
recoveryActionId: "33333333-3333-4333-8333-333333333333",
|
||
updatedAt: "2026-04-20T14:45:00.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
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,
|
||
},
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// MCP tool-approval fixtures (PAP-13745). A `request_confirmation` carrying a
|
||
// `payload.toolAction` block renders as the dedicated tool-approval card. The
|
||
// pending fixtures use a live `expiresAt` so the countdown renders meaningfully
|
||
// in Storybook; the destructive one sits inside the ~5-min urgent window.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function expiresInMinutes(minutes: number): string {
|
||
return new Date(Date.now() + minutes * 60000).toISOString();
|
||
}
|
||
|
||
const sheetsToolActionBase: RequestConfirmationToolActionPayload = {
|
||
version: 1,
|
||
actionRequestId: "aaaaaaa1-1111-4111-8111-1111111111a1",
|
||
invocationId: "bbbbbbb2-2222-4222-8222-2222222222b2",
|
||
toolName: "google_sheets.append_row",
|
||
toolDisplayName: "Append row to spreadsheet",
|
||
connectionId: "ccccccc3-3333-4333-8333-3333333333c3",
|
||
applicationId: "ddddddd4-4444-4444-8444-4444444444d4",
|
||
appDisplayName: "Google Sheets",
|
||
risk: "write" as const,
|
||
previewMarkdown:
|
||
"Add **1 row** to the **Q3 Growth Leads** sheet:\n\n"
|
||
+ "| Column | Value |\n| --- | --- |\n| Name | Priya Anand |\n| Company | Northwind |\n| Stage | Qualified |\n| Owner | growth-bot |",
|
||
argumentsSummaryJson: JSON.stringify(
|
||
{
|
||
spreadsheetId: "1AbC…xyz",
|
||
range: "Leads!A2:D2",
|
||
values: [["Priya Anand", "Northwind", "Qualified", "growth-bot"]],
|
||
apiKey: "[redacted]",
|
||
},
|
||
null,
|
||
2,
|
||
),
|
||
argumentsHash: "sha256:9f2c1a7be4d0c8a3",
|
||
expiresAt: expiresInMinutes(42),
|
||
};
|
||
|
||
function createToolActionConfirmationInteraction(
|
||
overrides: Partial<RequestConfirmationInteraction> & {
|
||
toolAction?: Partial<RequestConfirmationToolActionPayload>;
|
||
},
|
||
): RequestConfirmationInteraction {
|
||
const { toolAction: toolActionOverrides, payload, ...rest } = overrides;
|
||
return createRequestConfirmationInteraction({
|
||
id: "interaction-tool-action-default",
|
||
title: undefined,
|
||
summary: undefined,
|
||
createdByAgentId: "agent-codex",
|
||
payload: {
|
||
version: 1,
|
||
prompt: "Approve running this tool call?",
|
||
acceptLabel: "Approve & run",
|
||
rejectLabel: "Decline",
|
||
...payload,
|
||
toolAction: { ...sheetsToolActionBase, ...toolActionOverrides },
|
||
},
|
||
...rest,
|
||
});
|
||
}
|
||
|
||
export const pendingToolActionWriteInteraction = createToolActionConfirmationInteraction({
|
||
id: "interaction-tool-action-pending-write",
|
||
});
|
||
|
||
export const pendingToolActionDestructiveInteraction = createToolActionConfirmationInteraction({
|
||
id: "interaction-tool-action-pending-destructive",
|
||
toolAction: {
|
||
actionRequestId: "aaaaaaa5-5555-4555-8555-5555555555a5",
|
||
invocationId: "bbbbbbb6-6666-4666-8666-6666666666b6",
|
||
toolName: "google_sheets.delete_rows",
|
||
toolDisplayName: "Delete rows from spreadsheet",
|
||
risk: "destructive",
|
||
previewMarkdown:
|
||
"**Permanently delete 12 rows** (rows 30–41) from the **Q3 Growth Leads** sheet. "
|
||
+ "This cannot be undone.",
|
||
argumentsSummaryJson: JSON.stringify(
|
||
{ spreadsheetId: "1AbC…xyz", range: "Leads!A30:D41", rowCount: 12 },
|
||
null,
|
||
2,
|
||
),
|
||
argumentsHash: "sha256:1c4e77aa90b3f2d1",
|
||
expiresAt: expiresInMinutes(4),
|
||
},
|
||
});
|
||
|
||
export const runningToolActionInteraction = createToolActionConfirmationInteraction({
|
||
id: "interaction-tool-action-running",
|
||
status: "accepted",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "accepted",
|
||
toolAction: {
|
||
version: 1,
|
||
status: "approved",
|
||
updatedAt: "2026-04-20T15:02:00.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
export const executedToolActionInteraction = createToolActionConfirmationInteraction({
|
||
id: "interaction-tool-action-executed",
|
||
status: "accepted",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:12.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "accepted",
|
||
toolAction: {
|
||
version: 1,
|
||
status: "executed",
|
||
resultSummary: "Row 42 added to “Q3 Growth Leads”.",
|
||
resultHref: "https://docs.google.com/spreadsheets/d/1AbCxyz/edit#gid=0&range=A42",
|
||
updatedAt: "2026-04-20T15:02:12.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
export const failedToolActionInteraction = createToolActionConfirmationInteraction({
|
||
id: "interaction-tool-action-failed",
|
||
status: "accepted",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:09.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "accepted",
|
||
toolAction: {
|
||
version: 1,
|
||
status: "failed",
|
||
errorCode: "insufficient_permission",
|
||
errorMessage:
|
||
"The caller does not have permission to edit this spreadsheet (Google API 403). "
|
||
+ "Ask the sheet owner to grant edit access to the connected account.",
|
||
updatedAt: "2026-04-20T15:02:09.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
export const declinedToolActionInteraction = createToolActionConfirmationInteraction({
|
||
id: "interaction-tool-action-declined",
|
||
status: "rejected",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:01:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:01:00.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "rejected",
|
||
reason: "We don't add leads to this sheet manually — use the CRM sync instead.",
|
||
},
|
||
});
|
||
|
||
export const expiredToolActionInteraction = createToolActionConfirmationInteraction({
|
||
id: "interaction-tool-action-expired",
|
||
status: "expired",
|
||
updatedAt: new Date("2026-04-20T16:00:00.000Z"),
|
||
resolvedAt: new Date("2026-04-20T16:00:00.000Z"),
|
||
toolAction: {
|
||
expiresAt: "2026-04-20T16:00:00.000Z",
|
||
},
|
||
result: {
|
||
version: 1,
|
||
outcome: "superseded_by_comment",
|
||
toolAction: {
|
||
version: 1,
|
||
status: "expired",
|
||
updatedAt: "2026-04-20T16:00:00.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Secret-binding proposal fixtures. These mirror the server-owned
|
||
// `payload.secretProposal` block and intentionally contain display metadata
|
||
// only: no secret ids, values, fingerprints, or version material.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const secretProposalBase: RequestConfirmationSecretProposalPayload = {
|
||
version: 1,
|
||
proposalId: "eeeeeee5-5555-4555-8555-5555555555e5",
|
||
sourceSecretLabel: "OpenAI API key",
|
||
configPath: "access.evals_openai_api_key",
|
||
targetAgentId: "ffffffff-6666-4666-8666-6666666666f6",
|
||
targetAgentName: "EvalsEngineer",
|
||
justification:
|
||
"The evaluation runner needs the existing credential under its canonical config name.",
|
||
expiresAt: expiresInMinutes(14 * 24 * 60),
|
||
};
|
||
|
||
function createSecretProposalConfirmationInteraction(
|
||
overrides: Partial<RequestConfirmationInteraction> & {
|
||
secretProposal?: Partial<RequestConfirmationSecretProposalPayload>;
|
||
},
|
||
): RequestConfirmationInteraction {
|
||
const { secretProposal: secretProposalOverrides, payload, ...rest } = overrides;
|
||
return createRequestConfirmationInteraction({
|
||
id: "interaction-secret-proposal-default",
|
||
title: undefined,
|
||
summary: "Review a proposed alias for an existing secret binding.",
|
||
createdByAgentId: "agent-codex",
|
||
resolverPolicy: "human_only",
|
||
requestedResolverPolicy: "human_only",
|
||
effectiveResolverPolicy: "human_only",
|
||
payload: {
|
||
version: 1,
|
||
prompt: "Approve this secret binding?",
|
||
acceptLabel: "Approve & bind",
|
||
rejectLabel: "Reject",
|
||
allowDeclineReason: true,
|
||
...payload,
|
||
secretProposal: { ...secretProposalBase, ...secretProposalOverrides },
|
||
},
|
||
...rest,
|
||
});
|
||
}
|
||
|
||
export const pendingSecretProposalInteraction = createSecretProposalConfirmationInteraction({
|
||
id: "interaction-secret-proposal-pending",
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Connection-authorization fixtures (PAP-17835). Same interaction kind and the
|
||
// same server-addressed audience as any other confirmation; only the
|
||
// presentation payload is added, so the card never has to parse a title string
|
||
// to know what it is looking at.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function createConnectionAuthorizationInteraction(
|
||
overrides: Partial<RequestConfirmationInteraction> = {},
|
||
): RequestConfirmationInteraction {
|
||
const { payload, ...rest } = overrides;
|
||
return createRequestConfirmationInteraction({
|
||
id: "interaction-connection-authorization-default",
|
||
title: "Connect your Gmail to continue",
|
||
summary: "Outreach Agent needs your Gmail identity for work running as you.",
|
||
createdByAgentId: "agent-codex",
|
||
addresseeUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolverPolicy: "human_only",
|
||
requestedResolverPolicy: "human_only",
|
||
effectiveResolverPolicy: "human_only",
|
||
payload: {
|
||
version: 1,
|
||
prompt: "Connect your Gmail to continue",
|
||
acceptLabel: "Connect Gmail",
|
||
rejectLabel: "Not now",
|
||
...payload,
|
||
connectionAuthorization: {
|
||
version: 1,
|
||
providerName: "Gmail",
|
||
connectionName: null,
|
||
requestingAgentName: "Outreach Agent",
|
||
},
|
||
target: {
|
||
type: "custom",
|
||
key: "connection:gmail-abc:user:user-dotta",
|
||
label: "Connect Gmail",
|
||
href: "https://accounts.google.com/o/oauth2/v2/auth?client_id=paperclip",
|
||
},
|
||
},
|
||
...rest,
|
||
});
|
||
}
|
||
|
||
export const pendingConnectionAuthorizationInteraction = createConnectionAuthorizationInteraction({
|
||
id: "interaction-connection-authorization-pending",
|
||
});
|
||
|
||
export const resolvedConnectionAuthorizationInteraction = createConnectionAuthorizationInteraction({
|
||
id: "interaction-connection-authorization-resolved",
|
||
status: "accepted",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:03.000Z"),
|
||
result: { version: 1, outcome: "accepted" },
|
||
});
|
||
|
||
function createConnectionIntentInteraction(
|
||
overrides: Partial<ConnectionIntentInteraction> = {},
|
||
): ConnectionIntentInteraction {
|
||
return {
|
||
id: "interaction-connection-intent-default",
|
||
companyId: issueThreadInteractionFixtureMeta.companyId,
|
||
issueId: issueThreadInteractionFixtureMeta.issueId,
|
||
kind: "connection_intent",
|
||
title: "Connect Notion",
|
||
summary: "Researcher needs this connection to continue.",
|
||
status: "pending",
|
||
continuationPolicy: "wake_assignee",
|
||
createdByAgentId: "11111111-1111-4111-8111-111111111111",
|
||
createdByUserId: null,
|
||
resolvedByAgentId: null,
|
||
resolvedByUserId: null,
|
||
addresseeUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
createdAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
resolvedAt: null,
|
||
payload: {
|
||
version: 1,
|
||
serviceSlug: "notion",
|
||
serviceName: "Notion",
|
||
serviceLogoUrl: null,
|
||
requestingAgentId: "11111111-1111-4111-8111-111111111111",
|
||
requestingAgentName: "Researcher",
|
||
phase: "requested",
|
||
},
|
||
result: null,
|
||
resolverPolicy: "human_only",
|
||
requestedResolverPolicy: "human_only",
|
||
effectiveResolverPolicy: "human_only",
|
||
resolverPolicyProvenance: "explicit",
|
||
effectiveResolverPolicySource: "governed_action",
|
||
legacyResolverPolicyAliases: {
|
||
requested: "board_only",
|
||
effective: "board_only",
|
||
},
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
export const pendingConnectionIntentInteraction = createConnectionIntentInteraction();
|
||
export const retryConnectionIntentInteraction = createConnectionIntentInteraction({
|
||
id: "interaction-connection-intent-retry",
|
||
payload: {
|
||
version: 1,
|
||
serviceSlug: "notion",
|
||
serviceName: "Notion",
|
||
serviceLogoUrl: null,
|
||
requestingAgentId: "11111111-1111-4111-8111-111111111111",
|
||
requestingAgentName: "Researcher",
|
||
phase: "needs_retry",
|
||
},
|
||
});
|
||
export const authorizingConnectionIntentInteraction = createConnectionIntentInteraction({
|
||
id: "interaction-connection-intent-authorizing",
|
||
payload: {
|
||
version: 1,
|
||
serviceSlug: "notion",
|
||
serviceName: "Notion",
|
||
serviceLogoUrl: null,
|
||
requestingAgentId: "11111111-1111-4111-8111-111111111111",
|
||
requestingAgentName: "Researcher",
|
||
phase: "authorizing",
|
||
},
|
||
});
|
||
export const connectedConnectionIntentInteraction = createConnectionIntentInteraction({
|
||
id: "interaction-connection-intent-connected",
|
||
status: "accepted",
|
||
result: {
|
||
version: 1,
|
||
outcome: "connected",
|
||
connectionId: "22222222-2222-4222-8222-222222222222",
|
||
},
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:12:00.000Z"),
|
||
});
|
||
export const declinedConnectionIntentInteraction = createConnectionIntentInteraction({
|
||
id: "interaction-connection-intent-declined",
|
||
status: "rejected",
|
||
result: { version: 1, outcome: "declined", reason: "Not right now" },
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:12:00.000Z"),
|
||
});
|
||
export const supersededConnectionIntentInteraction = createConnectionIntentInteraction({
|
||
id: "interaction-connection-intent-superseded",
|
||
status: "expired",
|
||
result: { version: 1, outcome: "superseded" },
|
||
resolvedAt: new Date("2026-04-20T15:12:00.000Z"),
|
||
});
|
||
export const expiredConnectionIntentInteraction = createConnectionIntentInteraction({
|
||
id: "interaction-connection-intent-expired",
|
||
status: "expired",
|
||
result: { version: 1, outcome: "expired" },
|
||
resolvedAt: new Date("2026-04-20T15:12:00.000Z"),
|
||
});
|
||
|
||
export const executedSecretProposalInteraction = createSecretProposalConfirmationInteraction({
|
||
id: "interaction-secret-proposal-executed",
|
||
status: "accepted",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:03.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "accepted",
|
||
secretProposal: {
|
||
version: 1,
|
||
status: "executed",
|
||
updatedAt: "2026-04-20T15:02:03.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
export const failedSecretProposalInteraction = createSecretProposalConfirmationInteraction({
|
||
id: "interaction-secret-proposal-failed",
|
||
status: "accepted",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:03.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "accepted",
|
||
secretProposal: {
|
||
version: 1,
|
||
status: "failed",
|
||
errorCode: "binding_snapshot_stale",
|
||
updatedAt: "2026-04-20T15:02:03.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
export const rejectedSecretProposalInteraction = createSecretProposalConfirmationInteraction({
|
||
id: "interaction-secret-proposal-rejected",
|
||
status: "rejected",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "rejected",
|
||
reason: "Use the project-scoped credential instead.",
|
||
secretProposal: {
|
||
version: 1,
|
||
status: "rejected",
|
||
updatedAt: "2026-04-20T15:02:00.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
export const expiredSecretProposalInteraction = createSecretProposalConfirmationInteraction({
|
||
id: "interaction-secret-proposal-expired",
|
||
status: "expired",
|
||
resolvedAt: new Date("2026-05-04T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-05-04T15:02:00.000Z"),
|
||
secretProposal: { expiresAt: "2026-05-04T15:02:00.000Z" },
|
||
result: {
|
||
version: 1,
|
||
outcome: "superseded_by_newer_request",
|
||
secretProposal: {
|
||
version: 1,
|
||
status: "expired",
|
||
updatedAt: "2026-05-04T15:02:00.000Z",
|
||
},
|
||
},
|
||
});
|
||
|
||
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"),
|
||
});
|
||
|
||
// --- P4 governance / lifecycle card states (PAP-15427) ---
|
||
|
||
// Agent-addressed, agents-may-resolve pending confirmation: exercises the
|
||
// header policy badge + addressee chip.
|
||
export const agentAddressedRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-agent-addressed",
|
||
title: "Confirm the deploy window with the release agent",
|
||
summary:
|
||
"Directed to the release agent, who owns this response without waiting on the board.",
|
||
addresseeAgentId: "agent-codex",
|
||
});
|
||
|
||
// --- Explicit resolver restrictions (PAP-17280) ---
|
||
// Every card above is open by default; these four are the deliberate narrowings
|
||
// a requester or a company must ask for, one per audience row the card renders.
|
||
|
||
/** Independent review requested on purpose: the creator is excluded. */
|
||
export const notCreatorRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-not-creator",
|
||
title: "Independent review of the migration plan",
|
||
summary: "Asked for a second pair of eyes, so the agent that wrote the plan cannot approve it.",
|
||
requestedResolverPolicy: "not_creator",
|
||
});
|
||
|
||
/** A decision reserved for a person. */
|
||
export const humanOnlyRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-human-only",
|
||
title: "Approve the customer-facing announcement",
|
||
summary: "Reserved for a human on the board because it commits the organization publicly.",
|
||
requestedResolverPolicy: "human_only",
|
||
});
|
||
|
||
/** Open request narrowed by company interaction governance. */
|
||
export const companyCappedRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-company-capped",
|
||
title: "Confirm the data retention change",
|
||
summary: "Asked for an open audience; the organization caps this kind at a human decision.",
|
||
requestedResolverPolicy: "anyone",
|
||
effectiveResolverPolicy: "human_only",
|
||
effectiveResolverPolicySource: "company_cap",
|
||
});
|
||
|
||
/** Pre-migration card whose provenance cannot prove it was ever open. */
|
||
export const legacyRestrictedRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-legacy-restricted",
|
||
title: "Confirm the archived cleanup batch",
|
||
summary: "Created before Anyone became the default, so it stays restricted until re-created.",
|
||
requestedResolverPolicy: "not_creator",
|
||
resolverPolicyProvenance: "legacy_inherited_restriction",
|
||
});
|
||
|
||
// Confirmation resolved by an agent under governance: exercises the
|
||
// "Resolved by … / Agent" audit chip.
|
||
export const agentResolvedRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-agent-resolved",
|
||
title: "Approved by the release agent",
|
||
status: "accepted",
|
||
createdByAgentId: "agent-codex",
|
||
resolvedByAgentId: "agent-codex",
|
||
resolvedByRunId: "run-agent-resolve-1",
|
||
resolvedAt: new Date("2026-04-20T15:05:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:05:00.000Z"),
|
||
result: { version: 1, outcome: "accepted" },
|
||
});
|
||
|
||
// Withdrawn confirmation (status=cancelled + result.outcome=withdrawn): exercises
|
||
// the "Withdrawn by … / reason" footer.
|
||
export const withdrawnRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-withdrawn",
|
||
title: "Withdrawn: approve the plan",
|
||
status: "cancelled",
|
||
createdByAgentId: "agent-codex",
|
||
resolvedByAgentId: "agent-codex",
|
||
resolvedByRunId: "run-agent-withdraw-1",
|
||
resolvedAt: new Date("2026-04-20T15:10:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:10:00.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "withdrawn",
|
||
reason: "Plan superseded by a newer revision; no board decision needed.",
|
||
},
|
||
});
|
||
|
||
// Interaction auto-expired when its issue reached a terminal state.
|
||
export const issueClosedRequestConfirmationInteraction =
|
||
createRequestConfirmationInteraction({
|
||
id: "interaction-confirmation-issue-closed",
|
||
title: "Expired: confirm the migration cutover",
|
||
status: "expired",
|
||
// Local, not UTC. The expiry footer renders this in the machine's timezone,
|
||
// and 15:12Z on the 20th is already the 21st at UTC+9, which breaks the
|
||
// "Apr 20" assertion in IssueThreadInteractionCard.test.tsx.
|
||
resolvedAt: new Date(2026, 3, 20, 15, 12, 0, 0),
|
||
updatedAt: new Date(2026, 3, 20, 15, 12, 0, 0),
|
||
result: {
|
||
version: 1,
|
||
outcome: "issue_closed",
|
||
reason: "Issue was closed before the confirmation was resolved.",
|
||
},
|
||
});
|
||
|
||
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: "Reject",
|
||
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: "Reject",
|
||
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,
|
||
},
|
||
},
|
||
});
|
||
|
||
// --- Per-item verdicts (C3, PAP-13249) ---------------------------------
|
||
|
||
function createRequestItemVerdictsInteraction(
|
||
overrides: Partial<RequestItemVerdictsInteraction>,
|
||
): RequestItemVerdictsInteraction {
|
||
return {
|
||
id: "interaction-verdicts-default",
|
||
companyId: issueThreadInteractionFixtureMeta.companyId,
|
||
issueId: issueThreadInteractionFixtureMeta.issueId,
|
||
kind: "request_item_verdicts",
|
||
title: "Review 5 blog posts",
|
||
summary:
|
||
"This task drafted five blog posts. Approve the ones that are ready and reject the rest with a reason — each decision fans out on its own.",
|
||
status: "pending",
|
||
continuationPolicy: "wake_assignee",
|
||
createdByAgentId: "agent-codex",
|
||
createdByUserId: null,
|
||
resolvedByAgentId: null,
|
||
resolvedByUserId: null,
|
||
createdAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:02:00.000Z"),
|
||
resolvedAt: null,
|
||
payload: {
|
||
version: 1,
|
||
prompt: "Review the 5 blog posts this task drafted.",
|
||
detailsMarkdown:
|
||
"Each approved post publishes immediately; rejected posts go back for a revision pass with your reason attached.",
|
||
items: [
|
||
{
|
||
id: "post-spring-recap",
|
||
label: "Spring launch recap",
|
||
description: "820 words · product marketing",
|
||
previewMarkdown: "**Spring launch recap** — a warm retrospective on the Q1 launch and what shipped.",
|
||
href: "/PAP/issues/PAP-9001",
|
||
},
|
||
{
|
||
id: "post-changelog-digest",
|
||
label: "Monthly changelog digest",
|
||
description: "540 words · engineering",
|
||
previewMarkdown: "A tidy digest of the month's shipped changes, grouped by area.",
|
||
href: "/PAP/issues/PAP-9002",
|
||
},
|
||
{
|
||
id: "post-founder-note",
|
||
label: "Founder's note on reliability",
|
||
description: "1,100 words · leadership",
|
||
previewMarkdown: "A candid note on the reliability push and the road ahead.",
|
||
href: "/PAP/issues/PAP-9003",
|
||
},
|
||
{
|
||
id: "post-customer-story",
|
||
label: "Customer story: Northwind",
|
||
description: "760 words · customer marketing",
|
||
previewMarkdown: "How Northwind cut review time in half with the new workflow.",
|
||
href: "/PAP/issues/PAP-9004",
|
||
},
|
||
{
|
||
id: "post-hiring-push",
|
||
label: "We're hiring: platform engineers",
|
||
description: "420 words · recruiting",
|
||
previewMarkdown: "An open call for platform engineers to join the team.",
|
||
href: "/PAP/issues/PAP-9005",
|
||
},
|
||
],
|
||
verdicts: ["approve", "reject"],
|
||
requireReasonOn: ["reject"],
|
||
reasonLabel: "Why reject?",
|
||
allowBulkApprove: true,
|
||
supersedeOnUserComment: true,
|
||
},
|
||
result: null,
|
||
...overrides,
|
||
...resolverAudienceFields(overrides),
|
||
};
|
||
}
|
||
|
||
/** S1 — expanded, all pending. */
|
||
export const pendingRequestItemVerdictsInteraction = createRequestItemVerdictsInteraction({});
|
||
|
||
/** S3/S4 — partial: two items applied (one approved, one rejected), three still actionable. */
|
||
export const partialRequestItemVerdictsInteraction = createRequestItemVerdictsInteraction({
|
||
id: "interaction-verdicts-partial",
|
||
updatedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "resolved",
|
||
complete: false,
|
||
items: [
|
||
{
|
||
id: "post-spring-recap",
|
||
verdict: "approve",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
},
|
||
{
|
||
id: "post-changelog-digest",
|
||
verdict: "reject",
|
||
reason: "Tone is off-brand — too dry. Warm it up and re-submit.",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
},
|
||
],
|
||
},
|
||
});
|
||
|
||
/** S5 — complete: every item has a terminal verdict. */
|
||
export const completeRequestItemVerdictsInteraction = createRequestItemVerdictsInteraction({
|
||
id: "interaction-verdicts-complete",
|
||
status: "answered",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:14:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:14:00.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "resolved",
|
||
complete: true,
|
||
items: [
|
||
{
|
||
id: "post-spring-recap",
|
||
verdict: "approve",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
},
|
||
{
|
||
id: "post-changelog-digest",
|
||
verdict: "reject",
|
||
reason: "Tone is off-brand — too dry. Warm it up and re-submit.",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
},
|
||
{
|
||
id: "post-founder-note",
|
||
verdict: "approve",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:14:00.000Z"),
|
||
},
|
||
{
|
||
id: "post-customer-story",
|
||
verdict: "approve",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:14:00.000Z"),
|
||
},
|
||
{
|
||
id: "post-hiring-push",
|
||
verdict: "reject",
|
||
reason: "Hold the recruiting post until the req is approved.",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:14:00.000Z"),
|
||
},
|
||
],
|
||
},
|
||
});
|
||
|
||
/** S6 — superseded by a later comment after two items were already applied. */
|
||
export const supersededRequestItemVerdictsInteraction = createRequestItemVerdictsInteraction({
|
||
id: "interaction-verdicts-superseded",
|
||
status: "expired",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:16:00.000Z"),
|
||
updatedAt: new Date("2026-04-20T15:16:00.000Z"),
|
||
result: {
|
||
version: 1,
|
||
outcome: "superseded_by_comment",
|
||
complete: false,
|
||
commentId: "33333333-3333-4333-8333-333333333333",
|
||
items: [
|
||
{
|
||
id: "post-spring-recap",
|
||
verdict: "approve",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
},
|
||
{
|
||
id: "post-changelog-digest",
|
||
verdict: "reject",
|
||
reason: "Tone is off-brand — too dry. Warm it up and re-submit.",
|
||
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
||
resolvedAt: new Date("2026-04-20T15:08:00.000Z"),
|
||
},
|
||
],
|
||
},
|
||
});
|
||
|
||
/** S7 — long list (24 items) that virtualizes/paginates in the expanded view. */
|
||
const manyVerdictItems = Array.from({ length: 24 }, (_, index) => {
|
||
const number = index + 1;
|
||
return {
|
||
id: `draft-post-${number}`,
|
||
label: `Draft post #${number}`,
|
||
description: `${300 + number * 17} words · auto-generated series`,
|
||
};
|
||
});
|
||
|
||
export const manyItemsRequestItemVerdictsInteraction = createRequestItemVerdictsInteraction({
|
||
id: "interaction-verdicts-many",
|
||
title: "Review 24 generated posts",
|
||
summary: "A batch-generation task produced 24 posts. Decide them in passes; the card stays until all are decided.",
|
||
payload: {
|
||
version: 1,
|
||
prompt: "Review the 24 posts this batch produced.",
|
||
detailsMarkdown: "The expanded list scrolls. Approve all to accept the batch, or decide item by item.",
|
||
items: manyVerdictItems,
|
||
verdicts: ["approve", "reject"],
|
||
requireReasonOn: ["reject"],
|
||
reasonLabel: "Why reject?",
|
||
allowBulkApprove: true,
|
||
supersedeOnUserComment: true,
|
||
},
|
||
});
|
||
|
||
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,
|
||
];
|