Files
PaperClipAI/ui/src/fixtures/issueThreadInteractionFixtures.ts
T
DottaandPaperclip b43073d11f feat(connections): sync and group accounts managed by aggregators (#15254)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Connections give agents governed access to external tools.
> - Aggregator gateways can expose accounts that users already connected
upstream.
> - The Apps catalog did not show those accounts or their current
provider status.
> - Separate cards and setup tasks also made account ownership unclear.
> - This pull request discovers upstream accounts and groups them under
one app card.
> - Users can find connected apps while each provider keeps control of
its accounts.

## Linked Issues or Issue Description

**Subsystem affected**

Connections across the database, shared contracts, server, and board UI.

**Problem or motivation**

Users cannot see which apps are connected through a saved aggregator
gateway. Native and upstream accounts need one app card. Discovery must
preserve company, user, gateway, and credential boundaries.

**Proposed solution**

Sync account metadata from Composio, Arcade, and supported Executor
gateways. Keep upstream account management in each provider. Use source
chips and search to browse the catalog. Preserve native setup and the
gateway's existing access policy.

**Alternatives considered**

Creating a local executable connection for each upstream account would
duplicate authorization state. Using an agent task for routine Composio
setup would add an unnecessary step. The board now calls the saved
gateway directly for that setup.

**Roadmap alignment**

This extends the shipped Connected Apps and MCP Tool Gateway features in
ROADMAP.md. The duplicate search found no open PR for managed account
discovery.

Related work: Refs #13755, Refs #13941, Refs #14725, Refs #13855. Open
PR #12906 covers adjacent toolkit routing work.

## What Changed

- Add provider-neutral discovery, sync, and refresh APIs. Preserve the
Composio API paths.
- Cache observations by company, saved gateway, viewing user, and
credential version. Retain stale observations after failed or incomplete
scans.
- Add optional Arcade account sync credentials in the vault. Discover
Executor accounts through its supported inventory interface.
- Group native and upstream accounts in one app card. Imported account
menus open their provider. Gateway menus own refresh and sync setup.
- Add Paperclip, Composio, Arcade, Installed, and All chips. Show 50
catalog entries per page. Keep connected accounts above discovery. Keep
explicit provider searches scoped.
- Simplify Composio app setup and refresh its connected app list on the
gateway Permissions page.
- Add a compact agent access card and task creation defaults for
connection setup. Preserve explicit blocks and approval policies.
- Add two replay-safe migrations, service and UI tests, Storybook
journeys, and acceptance stories.

## Verification

- Passed the repository typecheck, full build, token gates, and
migration ordering check.
- Passed the focused provider adapter, connection interaction, and
catalog tests after rebasing onto master.
- Passed all nine database sync and migration replay tests using a
disposable database on the test-drive PostgreSQL cluster. Removed that
database after the run.
- Verified Arcade cursor pagination against its official Go SDK and
passed all eight adapter tests, including short and incomplete pages.
- Passed all 45 interaction tests after making the exact requested tools
and their Allowed/Ask first permissions visible before granting access.
Verified the compact card in Storybook.
- Passed the complete UI suite on the final code: 683 files and 7,432
tests, including the corrected Composio destination assertions. Passed
130 focused tests for the UUID, management-link, and health-status
corrections.
- Passed 22 Composio setup/sync tests, 23 connection-intent service
tests, and the connection migration test in separate disposable
databases. Database startup alone was substituted; the suites exercised
their real SQL and services.
- Passed all 10 OpenAPI route checks and the full-stack
connection-intent browser test, including scoped consent, agent
continuation, and task completion.
- The local full runner encountered embedded PostgreSQL startup failures
on this loaded macOS host. The earlier in-flight run also held the
pre-fix Arcade transform; a fresh run of the final provider suite
passes. The final-head CI is queued during GitHub’s active Actions
incident: https://www.githubstatus.com/. The previous run also lost
several runners simultaneously; its real catalog assertion failures are
fixed and the fresh complete UI suite passes.
- Tested the real test-drive server in the embedded browser with a live
Composio gateway. Detected Airtable and Circleback. Verified refresh
progress, account rows, source chips, search scope, and 50-entry
pagination.
- Arcade and Executor coverage uses provider fixtures. Live credentials
were unavailable.
- Storybook builds successfully and includes grouped native/provider
accounts, stale and unavailable discovery, optional Arcade setup, and
mobile states. The acceptance document records the simulated and live
coverage separately.

- Greptile reviewed final commit
`217b024c27b5933e773ce9419c4e92b1032042c6` at 5/5. All six review
threads are resolved, security scans pass, and the PR has no merge
conflicts. The outstanding remote checks are `ci / Select trusted
runner` and `review`, queued by GitHub. They need to complete before
merge.

## Risks

- Provider response changes can break inventory discovery. Failed scans
retain observations and show stale status.
- Composio scans only the supported catalog and can take time. Large
inventories run in the background with progress and a bounded lease.
- Arcade requires a project API key and user ID when the gateway cannot
supply them. This key is used only for discovery.
- Executor discovery depends on the server's exposed inventory tools.
Unsupported servers report unavailable discovery.
- Cached account rows do not grant access or create executable
connections. Gateway policies still govern tool use. Account deletion
and per-app authorization remain upstream.
- The migrations add tables and one nullable column. Replay preserves
existing rows and company-scoped foreign keys.

## Model Used

OpenAI Codex, based on GPT-6. The session does not expose a more
specific serving model ID or context limit. Used reasoning, repository
tools, code execution, and browser verification.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-05 14:57:04 -05:00

1644 lines
58 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
];
export const pendingConnectionAccessInteraction = createConnectionIntentInteraction({
id: "interaction-connection-access",
title: "Grant Composio access to Researcher?",
payload: { ...pendingConnectionIntentInteraction.payload, serviceSlug: "composio", serviceName: "Composio",
serviceLogoUrl: "/brands/apps/composio.svg",
accessRequest: { connectionId: "22222222-2222-4222-8222-222222222222", connectionName: "My Composio",
tools: [
{ catalogEntryId: "33333333-3333-4333-8333-333333333333", toolName: "COMPOSIO_SEARCH_TOOLS", versionHash: "fixture-v1", permission: "allowed" },
{ catalogEntryId: "44444444-4444-4444-8444-444444444444", toolName: "COMPOSIO_MANAGE_CONNECTIONS", versionHash: "fixture-v1", permission: "ask_first" },
],
},
},
});
export const grantedConnectionAccessInteraction = createConnectionIntentInteraction({
...pendingConnectionAccessInteraction, id: "interaction-connection-access-granted", status: "accepted",
result: { version: 1, outcome: "connected", connectionId: pendingConnectionAccessInteraction.payload.accessRequest!.connectionId },
});