mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agent runs often need provider credentials, API tokens, and other environment-bound secrets. > - Company-level secrets work for shared credentials, but they do not model values that should differ by human operator. > - Without a user-scoped model, a run can dispatch without knowing whether the responsible human has supplied the needed value. > - Paperclip also needs run attribution to make those user-scoped runtime checks deterministic and auditable. > - This pull request adds user-specific secret definitions, per-user values, environment bindings, responsible-user attribution, and runtime resolution gates. > - The benefit is that teams can define the secret once, let each user provide their own value, and block runs before dispatch when required user secrets or active definitions are unavailable. ## Linked Issues or Issue Description Refs #224 Refs #6057 This PR implements user-specific secret support as a core secret-management capability rather than a one-off adapter setting. It is related to existing public work on company secrets UI and runtime secret refs, but is distinct because the value is owned by the responsible user and resolved at run dispatch time. Related PR search before opening found existing secrets work such as #1550, #8256, #8614, #8634, and #8647; none of those add the full user-secret definition/value/runtime gate covered here. ## What Changed - Added user-secret definitions and per-user "My secrets" values, keeping stored values out of access metadata. - Added `user_secret_ref` environment bindings and UI affordances to pick them alongside existing secret refs. - Added responsible-user runtime resolution so user-secret refs resolve against the human responsible for the run. - Added pre-dispatch missing-secret gates so runs fail before adapter dispatch when required user values are absent or definitions are inactive. - Added low-trust allowlist hardening for user-secret runtime access. - Added issue, routine, run, and agent API key responsible-user attribution and fail-closed dispatch behavior when attribution cannot be resolved. - Added denial-copy mapping so responsible-user authorization failures surface as actionable run outcomes instead of opaque setup failures. - Added OpenAPI documentation for the user-secret routes. - Rebases cleanly on current `master`; migrations were renumbered incrementally as `0128_user_specific_secrets`, `0129_agent_api_key_responsible_user`, and `0130_run_responsible_user_invariant` after upstream `0126`/`0127` migrations. - Removed previously committed local design screenshots so the PR contains code/docs/tests only. ## Verification - PASS: PR head `2527febd106bcf3ca264ca0da7fca491084192d6` is based on `paperclipai/paperclip:master`. - PASS: `git diff --check` - PASS: `git diff --name-only public/master...HEAD | rg '^(pnpm-lock\\.yaml|\\.github/workflows/|screenshots/)' || true` produced no files. - PASS: migration journal audit confirmed unique indexes through `130` with tail entries `0126_issue_comment_derived_attribution`, `0127_environment_custom_images_instance_scoped`, `0128_user_specific_secrets`, `0129_agent_api_key_responsible_user`, and `0130_run_responsible_user_invariant`. - PASS: `pnpm --filter @paperclipai/ui typecheck` - PASS: `pnpm --filter @paperclipai/server typecheck` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-responsible-user-invariant.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-active-run-output-watchdog.test.ts src/__tests__/heartbeat-stale-queue-invalidation.test.ts src/__tests__/heartbeat-workspace-finalize-branch.test.ts src/__tests__/issue-monitor-scheduler.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-comment-wake-batching.test.ts src/__tests__/heartbeat-retry-scheduling.test.ts src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts src/__tests__/heartbeat-plugin-environment.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/low-trust-red-team-routes.test.ts` - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/secrets-service.test.ts` (55 tests) - PASS: `pnpm vitest run server/src/__tests__/secrets-routes.test.ts server/src/__tests__/secrets-service.test.ts` (89 tests after final Greptile cleanup fixes) - PASS: `pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-issue-liveness-escalation.test.ts` (17 tests after the final rebase CI fix) - PASS: focused server Vitest batches covering heartbeat recovery, project env, plugin env, routines, low-trust, pipelines, monitors, watchdog, and stale queue paths. - PASS: GitHub checks are green on `2527febd106bcf3ca264ca0da7fca491084192d6`, including Typecheck + Release Registry, Build, General tests, serialized server suites, e2e, Canary Dry Run, verify, security checks, and Greptile Review. - PASS: Greptile Review completed successfully on `2527febd106bcf3ca264ca0da7fca491084192d6` with Confidence Score 5/5, and GraphQL review-thread audit returned zero unresolved non-outdated threads. ## Risks - Runtime behavior now depends on a run having a correct responsible user; missing or incorrect responsibility assignment can block runs before adapter dispatch. - `user_secret_ref` bindings intentionally expose metadata without values, but UI/API callers may need to handle the new binding kind explicitly. - External secret providers and IAM policies are not automatically provisioned by this PR; operators still need to configure provider-side access for non-local vaults. - The PR is broad across db/shared/server/UI/runtime paths, so release validation should include both API and UI secret workflows before merge. - The migration renumbering is intentionally incremental after upstream migrations; the branch migrations use guarded column/table/index/constraint creation so users who tested the older draft numbering should not hit duplicate DDL for the existing objects. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex, GPT-5-based coding agent (`gpt-5`), Codex local adapter with shell/tool use and code execution. Context window and internal reasoning mode are not exposed by the runtime. ## 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 - [x] 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 - [x] 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> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
755 lines
26 KiB
TypeScript
755 lines
26 KiB
TypeScript
import type { LiveRunForIssue } from "../api/heartbeats";
|
|
import type {
|
|
IssueChatComment,
|
|
IssueChatTranscriptEntry,
|
|
} from "../lib/issue-chat-messages";
|
|
import type { IssueTimelineEvent } from "../lib/issue-timeline-events";
|
|
import type {
|
|
AskUserQuestionsInteraction,
|
|
RequestCheckboxConfirmationInteraction,
|
|
RequestConfirmationInteraction,
|
|
SuggestTasksInteraction,
|
|
} from "../lib/issue-thread-interactions";
|
|
|
|
export const issueThreadInteractionFixtureMeta = {
|
|
companyId: "company-storybook",
|
|
projectId: "project-board-ui",
|
|
issueId: "issue-thread-interactions",
|
|
currentUserId: "user-board",
|
|
} as const;
|
|
|
|
function createComment(overrides: Partial<IssueChatComment>): 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,
|
|
};
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
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: "Request changes",
|
|
rejectRequiresReason: false,
|
|
},
|
|
result: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export const pendingSuggestedTasksInteraction = createSuggestTasksInteraction({});
|
|
|
|
export const acceptedSuggestedTasksInteraction = createSuggestTasksInteraction({
|
|
id: "interaction-suggest-accepted",
|
|
status: "accepted",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:16:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:16:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
createdTasks: [
|
|
{
|
|
clientKey: "root-design",
|
|
issueId: "issue-created-1",
|
|
identifier: "PAP-1713",
|
|
title: "Prototype issue-thread interaction cards",
|
|
},
|
|
{
|
|
clientKey: "child-stories",
|
|
issueId: "issue-created-2",
|
|
identifier: "PAP-1714",
|
|
title: "Add Storybook coverage for acceptance and rejection states",
|
|
parentIssueId: "issue-created-1",
|
|
parentIdentifier: "PAP-1713",
|
|
},
|
|
{
|
|
clientKey: "child-mixed-thread",
|
|
issueId: "issue-created-3",
|
|
identifier: "PAP-1715",
|
|
title: "Prototype the mixed thread feed",
|
|
parentIssueId: "issue-created-1",
|
|
parentIdentifier: "PAP-1713",
|
|
},
|
|
{
|
|
clientKey: "hidden-follow-up",
|
|
issueId: "issue-created-4",
|
|
identifier: "PAP-1716",
|
|
title: "Follow-up polish on spacing and answered summaries",
|
|
parentIssueId: "issue-created-3",
|
|
parentIdentifier: "PAP-1715",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
export const rejectedSuggestedTasksInteraction = createSuggestTasksInteraction({
|
|
id: "interaction-suggest-rejected",
|
|
status: "rejected",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:17:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:17:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
rejectionReason:
|
|
"Keep the first pass tighter. The hidden follow-on work is useful, but the acceptance story should stay focused on one visible root and one visible child.",
|
|
},
|
|
});
|
|
|
|
export const pendingAskUserQuestionsInteraction = createAskUserQuestionsInteraction({});
|
|
|
|
export const answeredAskUserQuestionsInteraction = createAskUserQuestionsInteraction({
|
|
id: "interaction-questions-answered",
|
|
status: "answered",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:24:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:24:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
answers: [
|
|
{
|
|
questionId: "collapse-depth",
|
|
optionIds: ["visible-root"],
|
|
},
|
|
{
|
|
questionId: "post-submit-summary",
|
|
optionIds: ["answers-inline", "summary-note", "resolver-meta"],
|
|
},
|
|
],
|
|
summaryMarkdown: [
|
|
"- Keep visible child tasks expanded when they are part of the main review path.",
|
|
"- Preserve inline answer chips and resolver metadata in the answered state.",
|
|
"- Add a short summary note so future reviewers understand the operator's intent without replaying the form.",
|
|
].join("\n"),
|
|
},
|
|
});
|
|
|
|
export const commentExpiredAskUserQuestionsInteraction = createAskUserQuestionsInteraction({
|
|
id: "interaction-questions-expired-comment",
|
|
status: "expired",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:25:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:25:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
answers: [],
|
|
expirationReason: "superseded_by_comment",
|
|
commentId: "22222222-2222-4222-8222-222222222222",
|
|
summaryMarkdown: null,
|
|
},
|
|
});
|
|
|
|
export const pendingRequestConfirmationInteraction = createRequestConfirmationInteraction({});
|
|
|
|
export const genericPendingRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-generic-pending",
|
|
title: "Confirm next step",
|
|
summary: "The responsible needs a lightweight yes or no before continuing.",
|
|
continuationPolicy: "none",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Continue with the current approach?",
|
|
},
|
|
});
|
|
|
|
export const optionalDeclineRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-optional-decline",
|
|
continuationPolicy: "none",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Use the smaller implementation path?",
|
|
acceptLabel: "Confirm",
|
|
rejectLabel: "Decline",
|
|
rejectRequiresReason: false,
|
|
declineReasonPlaceholder: "Optional: tell the agent what you'd change.",
|
|
},
|
|
});
|
|
|
|
export const disabledDeclineReasonRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-no-decline-reason",
|
|
continuationPolicy: "none",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Close this low-risk follow-up as unnecessary?",
|
|
acceptLabel: "Close it",
|
|
rejectLabel: "Keep it",
|
|
allowDeclineReason: false,
|
|
},
|
|
});
|
|
|
|
export const acceptedRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-accepted",
|
|
status: "accepted",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:34:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:34:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
outcome: "accepted",
|
|
},
|
|
});
|
|
|
|
export const planApprovalAcceptedRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-plan-accepted",
|
|
status: "accepted",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:34:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:34:00.000Z"),
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Approve the plan and let the responsible start implementation?",
|
|
acceptLabel: "Approve plan",
|
|
rejectLabel: "Request changes",
|
|
rejectRequiresReason: true,
|
|
declineReasonPlaceholder: "Optional: what would you like revised?",
|
|
target: {
|
|
type: "issue_document",
|
|
issueId: issueThreadInteractionFixtureMeta.issueId,
|
|
key: "plan",
|
|
revisionId: "11111111-1111-4111-8111-111111111111",
|
|
revisionNumber: 4,
|
|
},
|
|
},
|
|
result: {
|
|
version: 1,
|
|
outcome: "accepted",
|
|
},
|
|
});
|
|
|
|
export const rejectedRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-rejected",
|
|
status: "rejected",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:36:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:36:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
outcome: "rejected",
|
|
reason: "Split the migration and UI work into separate reviewable steps.",
|
|
},
|
|
});
|
|
|
|
export const rejectedNoReasonRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-rejected-no-reason",
|
|
status: "rejected",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:37:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:37:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
outcome: "rejected",
|
|
reason: null,
|
|
},
|
|
});
|
|
|
|
export const commentExpiredRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-expired-comment",
|
|
status: "expired",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:38:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:38:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
outcome: "superseded_by_comment",
|
|
commentId: "22222222-2222-4222-8222-222222222222",
|
|
},
|
|
});
|
|
|
|
export const staleTargetRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-expired-target",
|
|
status: "expired",
|
|
resolvedByAgentId: "agent-codex",
|
|
resolvedAt: new Date("2026-04-20T14:40:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:40:00.000Z"),
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Approve the plan and let the responsible start implementation?",
|
|
acceptLabel: "Approve plan",
|
|
rejectLabel: "Request revisions",
|
|
rejectRequiresReason: true,
|
|
target: {
|
|
type: "issue_document",
|
|
issueId: issueThreadInteractionFixtureMeta.issueId,
|
|
key: "plan",
|
|
revisionId: "44444444-4444-4444-8444-444444444444",
|
|
revisionNumber: 4,
|
|
},
|
|
},
|
|
result: {
|
|
version: 1,
|
|
outcome: "stale_target",
|
|
staleTarget: {
|
|
type: "issue_document",
|
|
issueId: issueThreadInteractionFixtureMeta.issueId,
|
|
key: "plan",
|
|
revisionId: "11111111-1111-4111-8111-111111111111",
|
|
revisionNumber: 3,
|
|
},
|
|
},
|
|
});
|
|
|
|
export const failedRequestConfirmationInteraction = createRequestConfirmationInteraction({
|
|
id: "interaction-confirmation-failed",
|
|
status: "failed",
|
|
updatedAt: new Date("2026-04-20T14:42:00.000Z"),
|
|
});
|
|
|
|
export const pendingRequestCheckboxConfirmationInteraction =
|
|
createRequestCheckboxConfirmationInteraction({});
|
|
|
|
export const boundedRequestCheckboxConfirmationInteraction =
|
|
createRequestCheckboxConfirmationInteraction({
|
|
id: "interaction-checkbox-bounded",
|
|
title: "Pick the regions to deploy first",
|
|
summary: "Choose between two and three regions for the Phase 1 rollout.",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Which regions should we deploy to initially?",
|
|
detailsMarkdown: "Select at least 2 and at most 3 regions.",
|
|
options: [
|
|
{ id: "us-west", label: "US West (Oregon)", description: "Lowest latency for our primary user base." },
|
|
{ id: "us-east", label: "US East (Virginia)", description: "Redundancy and east coast coverage." },
|
|
{ id: "eu-west", label: "EU West (Ireland)", description: "GDPR compliance and European users." },
|
|
{ id: "ap-southeast", label: "AP Southeast (Singapore)", description: "Asia-Pacific expansion." },
|
|
],
|
|
defaultSelectedOptionIds: ["us-west", "us-east"],
|
|
minSelected: 2,
|
|
maxSelected: 3,
|
|
acceptLabel: "Confirm regions",
|
|
rejectLabel: "Reconsider",
|
|
rejectRequiresReason: true,
|
|
rejectReasonLabel: "What should change about the region set?",
|
|
},
|
|
});
|
|
|
|
export const acceptedRequestCheckboxConfirmationInteraction =
|
|
createRequestCheckboxConfirmationInteraction({
|
|
id: "interaction-checkbox-accepted",
|
|
status: "accepted",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:49:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:49:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
outcome: "accepted",
|
|
selectedOptionIds: ["draft-march-report", "draft-spec-v1"],
|
|
},
|
|
});
|
|
|
|
const manyOptionList = Array.from({ length: 100 }, (_, index) => {
|
|
const number = index + 1;
|
|
return {
|
|
id: `record-${number}`,
|
|
label: `Customer record #${number}`,
|
|
description: number % 4 === 0
|
|
? "Flagged as a possible duplicate during the last import."
|
|
: undefined,
|
|
};
|
|
});
|
|
|
|
export const manyOptionsRequestCheckboxConfirmationInteraction =
|
|
createRequestCheckboxConfirmationInteraction({
|
|
id: "interaction-checkbox-many",
|
|
title: "Select the customer records to archive",
|
|
summary: "The cleanup job found 100 stale customer records. Confirm which ones to archive.",
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Check every customer record you want archived.",
|
|
detailsMarkdown: "The list scrolls. Use Select all / Clear selection to move quickly.",
|
|
options: manyOptionList,
|
|
defaultSelectedOptionIds: [],
|
|
minSelected: 0,
|
|
maxSelected: null,
|
|
acceptLabel: "Archive selected",
|
|
rejectLabel: "Request changes",
|
|
rejectRequiresReason: false,
|
|
},
|
|
});
|
|
|
|
export const acceptedManyRequestCheckboxConfirmationInteraction =
|
|
createRequestCheckboxConfirmationInteraction({
|
|
id: "interaction-checkbox-many-accepted",
|
|
status: "accepted",
|
|
title: "Select the customer records to archive",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:52:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:52:00.000Z"),
|
|
payload: manyOptionsRequestCheckboxConfirmationInteraction.payload,
|
|
result: {
|
|
version: 1,
|
|
outcome: "accepted",
|
|
selectedOptionIds: manyOptionList.slice(0, 42).map((option) => option.id),
|
|
},
|
|
});
|
|
|
|
export const rejectedRequestCheckboxConfirmationInteraction =
|
|
createRequestCheckboxConfirmationInteraction({
|
|
id: "interaction-checkbox-rejected",
|
|
status: "rejected",
|
|
resolvedByUserId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
resolvedAt: new Date("2026-04-20T14:50:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:50:00.000Z"),
|
|
result: {
|
|
version: 1,
|
|
outcome: "rejected",
|
|
reason: "Don't delete anything yet — let me confirm with the data owner first.",
|
|
},
|
|
});
|
|
|
|
export const staleTargetRequestCheckboxConfirmationInteraction =
|
|
createRequestCheckboxConfirmationInteraction({
|
|
id: "interaction-checkbox-stale",
|
|
status: "expired",
|
|
resolvedByAgentId: "agent-codex",
|
|
resolvedAt: new Date("2026-04-20T14:51:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:51:00.000Z"),
|
|
payload: {
|
|
version: 1,
|
|
prompt: "Check the draft documents you want me to delete.",
|
|
acceptLabel: "Delete selected",
|
|
rejectLabel: "Request changes",
|
|
options: [
|
|
{ id: "draft-march-report", label: "Old draft report" },
|
|
{ id: "draft-spec-v1", label: "Spec v1 (superseded)" },
|
|
],
|
|
target: {
|
|
type: "issue_document",
|
|
issueId: issueThreadInteractionFixtureMeta.issueId,
|
|
key: "plan",
|
|
revisionId: "44444444-4444-4444-8444-444444444444",
|
|
revisionNumber: 4,
|
|
},
|
|
},
|
|
result: {
|
|
version: 1,
|
|
outcome: "stale_target",
|
|
staleTarget: {
|
|
type: "issue_document",
|
|
issueId: issueThreadInteractionFixtureMeta.issueId,
|
|
key: "plan",
|
|
revisionId: "11111111-1111-4111-8111-111111111111",
|
|
revisionNumber: 3,
|
|
},
|
|
},
|
|
});
|
|
|
|
export const issueThreadInteractionComments: IssueChatComment[] = [
|
|
createComment({
|
|
id: "comment-thread-board",
|
|
body: "Pressure-test first-class issue-thread interactions before we touch persistence. I want to see the cards in the real feed, not in a disconnected mock.",
|
|
createdAt: new Date("2026-04-20T14:02:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:02:00.000Z"),
|
|
}),
|
|
createComment({
|
|
id: "comment-thread-agent",
|
|
authorAgentId: "agent-codex",
|
|
authorUserId: null,
|
|
body: "I found the existing issue chat surface and I am adding prototype-only interaction records so the Storybook review can happen before persistence work.",
|
|
createdAt: new Date("2026-04-20T14:09:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T14:09:00.000Z"),
|
|
runId: "run-thread-interaction",
|
|
runAgentId: "agent-codex",
|
|
}),
|
|
];
|
|
|
|
export const issueThreadInteractionEvents: IssueTimelineEvent[] = [
|
|
{
|
|
id: "event-thread-checkout",
|
|
createdAt: new Date("2026-04-20T14:01:00.000Z"),
|
|
actorType: "user",
|
|
actorId: issueThreadInteractionFixtureMeta.currentUserId,
|
|
statusChange: {
|
|
from: "todo",
|
|
to: "in_progress",
|
|
},
|
|
},
|
|
];
|
|
|
|
export const issueThreadInteractionLiveRuns: LiveRunForIssue[] = [
|
|
{
|
|
id: "run-thread-live",
|
|
status: "running",
|
|
invocationSource: "manual",
|
|
triggerDetail: null,
|
|
startedAt: "2026-04-20T14:26:00.000Z",
|
|
finishedAt: null,
|
|
createdAt: "2026-04-20T14:26:00.000Z",
|
|
agentId: "agent-codex",
|
|
agentName: "CodexCoder",
|
|
adapterType: "codex_local",
|
|
},
|
|
];
|
|
|
|
export const issueThreadInteractionTranscriptsByRunId = new Map<
|
|
string,
|
|
readonly IssueChatTranscriptEntry[]
|
|
>([
|
|
[
|
|
"run-thread-live",
|
|
[
|
|
{
|
|
kind: "assistant",
|
|
ts: "2026-04-20T14:26:02.000Z",
|
|
text: "Wiring the prototype interaction cards into the same issue feed that already renders comments and live runs.",
|
|
},
|
|
{
|
|
kind: "thinking",
|
|
ts: "2026-04-20T14:26:04.000Z",
|
|
text: "Need to keep the payload shapes local to the UI layer so Phase 0 stays non-persistent.",
|
|
},
|
|
],
|
|
],
|
|
]);
|
|
|
|
export const mixedIssueThreadInteractions = [
|
|
acceptedSuggestedTasksInteraction,
|
|
pendingRequestConfirmationInteraction,
|
|
pendingAskUserQuestionsInteraction,
|
|
];
|