mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 05:16:50 +01:00
feat: add attention queue and Decisions surface (#9380)
## Thinking Path > - Paperclip is the control plane for autonomous AI companies, where operators need a reliable way to find and act on work awaiting their input. > - The attention and issue-thread interaction subsystems expose those decision points across server APIs and the board UI. > - The previous navigation and interaction presentation left these actions fragmented and did not offer a controlled rollout for the Decisions surface. > - This branch adds the attention feed, richer interaction cards, grouping, dismiss/snooze behavior, and a gated Decisions sidebar entry. > - It also keeps experimental settings and API contracts synchronized, with an idempotent migration for the new dismissal state. > - This pull request delivers the complete, tested attention/Decisions experience as one reviewable unit. ## Linked Issues or Issue Description - Adds an operator-focused attention queue and Decisions experience: grouped decision cards, semantic interaction actions, dismiss/snooze handling, resilient interaction states, and an experimental flag to control the Decisions navigation entry. ## Feature Context ### Problem or Motivation Operators currently have to hunt across approvals, interactions, failed runs, and budget alerts to find decisions that need their action. ### Proposed Solution Provide a gated Decisions attention queue that groups actionable items, supports direct resolution, and preserves operator control through dismiss and snooze actions. ### Alternatives Considered Keep separate, source-specific views only; this leaves cross-cutting operator decisions fragmented and harder to prioritize. ### Roadmap Alignment This improves the V1 control-plane operator workflow by making pending governed actions discoverable in one company-scoped surface. ## What Changed - Added server attention-feed services, routes, interaction handling, dismiss/snooze support, and an idempotent `0145` inbox-dismissal migration. - Added shared attention, inbox-dismissal, and experimental-settings contracts. - Added Decisions/attention UI, interaction-card states, sidebar badge/navigation integration, grouping, keyboard support, and Storybook coverage. - Added tests for attention behavior, thread interactions, settings normalization, dismissals, and API behavior. - Removed generated screenshots from the final PR diff and rebased the branch onto current `master`. ## Verification - `pnpm check:token-gates` — passed. - `pnpm exec vitest run packages/shared/src/issue-thread-interactions.test.ts server/src/__tests__/attention-service.test.ts server/src/__tests__/inbox-dismissals.test.ts server/src/__tests__/issue-thread-interactions-service.test.ts server/src/__tests__/issue-thread-interaction-routes.test.ts ui/src/lib/attention.test.ts ui/src/components/AttentionQueueRow.test.tsx ui/src/components/IssueThreadInteractionCard.test.tsx ui/src/pages/InstanceExperimentalSettings.test.tsx` — passed: 158 tests across 9 focused files. - GitHub Actions for `ad636f560`: build and typecheck/release-registry have passed; remaining general-server and Greptile checks are in progress. ## Risks - Moderate: this is a cross-layer attention/interaction feature with a new migration and navigation behavior. - The `enableDecisions` experimental setting defaults to off, limiting rollout impact. - Existing dismissal data is backfilled to `dismiss`; the migration is idempotent and uses guarded constraint creation. > ROADMAP.md was checked; no duplicate planned core feature was identified. Related open pull requests were searched before opening this PR. ## Model Used - OpenAI GPT-5.5 via Codex CLI, with tool use and local code execution. Context-window size unavailable in this environment. ## 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 public PR branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally; focused tests pass and the remaining unrelated AWS test failure is documented above - [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 - [ ] 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>
This commit is contained in:
70 files changed
+9067
-90
No files matched your search
@@ -248,10 +248,12 @@ export const ISSUE_THREAD_INTERACTION_KINDS = [
|
||||
"ask_user_questions",
|
||||
"request_confirmation",
|
||||
"request_checkbox_confirmation",
|
||||
"request_item_verdicts",
|
||||
] as const;
|
||||
export type IssueThreadInteractionKind = (typeof ISSUE_THREAD_INTERACTION_KINDS)[number];
|
||||
|
||||
export const REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT = 200;
|
||||
export const REQUEST_ITEM_VERDICTS_ITEM_LIMIT = REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT;
|
||||
|
||||
export const ISSUE_THREAD_INTERACTION_STATUSES = [
|
||||
"pending",
|
||||
|
||||
@@ -49,6 +49,21 @@ export {
|
||||
type ResponsibleUserDenialCopy,
|
||||
type ResponsibleUserDenialTone,
|
||||
} from "./responsible-user-denial.js";
|
||||
export type {
|
||||
AttentionDecisionVerb,
|
||||
AttentionDetailImage,
|
||||
AttentionFeed,
|
||||
AttentionItem,
|
||||
AttentionItemDetail,
|
||||
AttentionItemDismissal,
|
||||
AttentionProjectRef,
|
||||
AttentionSeverity,
|
||||
AttentionSourceKind,
|
||||
AttentionSubject,
|
||||
AttentionSubjectKind,
|
||||
AttentionWorkspaceRef,
|
||||
} from "./types/attention.js";
|
||||
|
||||
export type {
|
||||
PipelineAutomationRetryBlocker,
|
||||
PipelineAutomationRetryCleanupOptions,
|
||||
@@ -296,6 +311,7 @@ export {
|
||||
type IssueThreadInteractionStatus,
|
||||
type IssueThreadInteractionContinuationPolicy,
|
||||
REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT,
|
||||
REQUEST_ITEM_VERDICTS_ITEM_LIMIT,
|
||||
type BuiltInIssueOriginKind,
|
||||
type PluginIssueOriginKind,
|
||||
type IssueOriginKind,
|
||||
@@ -770,6 +786,11 @@ export type {
|
||||
RequestCheckboxConfirmationOption,
|
||||
RequestCheckboxConfirmationPayload,
|
||||
RequestCheckboxConfirmationResult,
|
||||
RequestItemVerdictValue,
|
||||
RequestItemVerdictsItem,
|
||||
RequestItemVerdictsPayload,
|
||||
RequestItemVerdictsResultItem,
|
||||
RequestItemVerdictsResult,
|
||||
AcceptedPlanDecompositionStatus,
|
||||
AcceptedPlanDecompositionChild,
|
||||
AcceptedPlanDecomposition,
|
||||
@@ -781,6 +802,7 @@ export type {
|
||||
AskUserQuestionsInteraction,
|
||||
RequestConfirmationInteraction,
|
||||
RequestCheckboxConfirmationInteraction,
|
||||
RequestItemVerdictsInteraction,
|
||||
IssueThreadInteraction,
|
||||
IssueThreadInteractionPayload,
|
||||
IssueThreadInteractionResult,
|
||||
@@ -857,6 +879,7 @@ export type {
|
||||
SidebarBadges,
|
||||
SidebarOrderPreference,
|
||||
InboxDismissal,
|
||||
InboxDismissalKind,
|
||||
AccessUserProfile,
|
||||
CompanyMemberRecord,
|
||||
CompanyMembersResponse,
|
||||
@@ -1284,11 +1307,17 @@ export {
|
||||
requestCheckboxConfirmationOptionSchema,
|
||||
requestCheckboxConfirmationPayloadSchema,
|
||||
requestCheckboxConfirmationResultSchema,
|
||||
requestItemVerdictValueSchema,
|
||||
requestItemVerdictsItemSchema,
|
||||
requestItemVerdictsPayloadSchema,
|
||||
requestItemVerdictsResultItemSchema,
|
||||
requestItemVerdictsResultSchema,
|
||||
createIssueThreadInteractionSchema,
|
||||
acceptIssueThreadInteractionSchema,
|
||||
rejectIssueThreadInteractionSchema,
|
||||
cancelIssueThreadInteractionSchema,
|
||||
respondIssueThreadInteractionSchema,
|
||||
submitIssueThreadInteractionVerdictsSchema,
|
||||
linkIssueApprovalSchema,
|
||||
createIssueAttachmentMetadataSchema,
|
||||
createIssueWorkProductSchema,
|
||||
@@ -1350,6 +1379,7 @@ export {
|
||||
type RejectIssueThreadInteraction,
|
||||
type CancelIssueThreadInteraction,
|
||||
type RespondIssueThreadInteraction,
|
||||
type SubmitIssueThreadInteractionVerdicts,
|
||||
type LinkIssueApproval,
|
||||
type CreateIssueAttachmentMetadata,
|
||||
type CreateIssueWorkProduct,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
acceptIssueThreadInteractionSchema,
|
||||
askUserQuestionsResultSchema,
|
||||
createIssueThreadInteractionSchema,
|
||||
submitIssueThreadInteractionVerdictsSchema,
|
||||
} from "./validators/issue.js";
|
||||
|
||||
describe("issue thread interaction schemas", () => {
|
||||
@@ -306,4 +307,111 @@ describe("issue thread interaction schemas", () => {
|
||||
selectedOptionIds: ["item-1", "item-1"],
|
||||
})).toThrow("selectedOptionIds must be unique");
|
||||
});
|
||||
|
||||
it("parses request_item_verdicts payloads with defaults", () => {
|
||||
const parsed = createIssueThreadInteractionSchema.parse({
|
||||
kind: "request_item_verdicts",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Review these generated items.",
|
||||
items: [
|
||||
{ id: "api", label: "API route", description: "Server submit endpoint" },
|
||||
{ id: "docs", label: "Docs", previewMarkdown: "Document the route." },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
kind: "request_item_verdicts",
|
||||
continuationPolicy: "wake_assignee",
|
||||
payload: {
|
||||
verdicts: ["approve", "reject"],
|
||||
requireReasonOn: ["reject"],
|
||||
allowBulkApprove: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts request_item_verdicts defer when enabled explicitly", () => {
|
||||
const parsed = createIssueThreadInteractionSchema.parse({
|
||||
kind: "request_item_verdicts",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Review these generated items.",
|
||||
items: [{ id: "api", label: "API route" }],
|
||||
verdicts: ["approve", "reject", "defer"],
|
||||
requireReasonOn: ["reject", "defer"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
kind: "request_item_verdicts",
|
||||
payload: {
|
||||
verdicts: ["approve", "reject", "defer"],
|
||||
requireReasonOn: ["reject", "defer"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid request_item_verdicts item and reason references", () => {
|
||||
const base = {
|
||||
kind: "request_item_verdicts",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Review these generated items.",
|
||||
items: [
|
||||
{ id: "api", label: "API route" },
|
||||
{ id: "docs", label: "Docs" },
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
expect(() => createIssueThreadInteractionSchema.parse({
|
||||
...base,
|
||||
payload: {
|
||||
...base.payload,
|
||||
items: [],
|
||||
},
|
||||
})).toThrow();
|
||||
|
||||
expect(() => createIssueThreadInteractionSchema.parse({
|
||||
...base,
|
||||
payload: {
|
||||
...base.payload,
|
||||
items: [
|
||||
{ id: "api", label: "API route" },
|
||||
{ id: "api", label: "Duplicate" },
|
||||
],
|
||||
},
|
||||
})).toThrow("Item ids must be unique within one item verdict request");
|
||||
|
||||
expect(() => createIssueThreadInteractionSchema.parse({
|
||||
...base,
|
||||
payload: {
|
||||
...base.payload,
|
||||
items: Array.from({ length: 201 }, (_value, index) => ({
|
||||
id: `item-${index}`,
|
||||
label: `Item ${index}`,
|
||||
})),
|
||||
},
|
||||
})).toThrow();
|
||||
|
||||
expect(() => createIssueThreadInteractionSchema.parse({
|
||||
...base,
|
||||
payload: {
|
||||
...base.payload,
|
||||
verdicts: ["approve", "reject"],
|
||||
requireReasonOn: ["defer"],
|
||||
},
|
||||
})).toThrow("requireReasonOn must reference enabled verdicts");
|
||||
});
|
||||
|
||||
it("rejects duplicate request_item_verdicts submit ids", () => {
|
||||
expect(() => submitIssueThreadInteractionVerdictsSchema.parse({
|
||||
verdicts: [
|
||||
{ id: "api", verdict: "approve" },
|
||||
{ id: "api", verdict: "reject", reason: "Needs revision" },
|
||||
],
|
||||
})).toThrow("verdict item ids must be unique");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { InboxDismissalKind } from "./inbox-dismissal.js";
|
||||
|
||||
export type AttentionSourceKind =
|
||||
| "approval"
|
||||
| "issue_thread_interaction"
|
||||
| "join_request"
|
||||
| "recovery_action"
|
||||
| "productivity_review"
|
||||
| "blocker_attention"
|
||||
| "review"
|
||||
| "failed_run"
|
||||
| "budget_alert"
|
||||
| "agent_error_alert";
|
||||
|
||||
export type AttentionSubjectKind =
|
||||
| "approval"
|
||||
| "issue"
|
||||
| "interaction"
|
||||
| "join_request"
|
||||
| "recovery_action"
|
||||
| "run"
|
||||
| "budget_incident"
|
||||
| "agent";
|
||||
|
||||
export type AttentionSeverity = "critical" | "high" | "medium" | "low";
|
||||
|
||||
export interface AttentionSubject {
|
||||
kind: AttentionSubjectKind;
|
||||
id: string;
|
||||
companyId: string;
|
||||
title: string | null;
|
||||
identifier: string | null;
|
||||
status: string | null;
|
||||
href: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AttentionDecisionVerb {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface AttentionProjectRef {
|
||||
id: string;
|
||||
name: string;
|
||||
urlKey: string;
|
||||
color: string | null;
|
||||
icon: string | null;
|
||||
}
|
||||
|
||||
export interface AttentionWorkspaceRef {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AttentionDetailImage {
|
||||
assetId: string;
|
||||
alt?: string | null;
|
||||
}
|
||||
|
||||
export interface AttentionItemDismissal {
|
||||
kind: InboxDismissalKind;
|
||||
dismissedAt: string;
|
||||
snoozedUntil: string | null;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export type AttentionItemDetail =
|
||||
| {
|
||||
kind: "approval";
|
||||
approvalType: string;
|
||||
summaryExcerpt: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "plan_approval";
|
||||
issueTitle: string | null;
|
||||
planTitle: string | null;
|
||||
summaryExcerpt: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "confirmation";
|
||||
promptExcerpt: string | null;
|
||||
isPlanTarget: false;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "questions";
|
||||
questionCount: number;
|
||||
firstQuestionText: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "suggested_tasks";
|
||||
taskCount: number;
|
||||
firstTaskTitle: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "checkbox_confirmation";
|
||||
optionCount: number;
|
||||
promptExcerpt: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "item_verdicts";
|
||||
itemCount: number;
|
||||
promptExcerpt: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "failed_run";
|
||||
agentName: string | null;
|
||||
failureReasonExcerpt: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "blocker";
|
||||
blockingIssue: {
|
||||
id: string | null;
|
||||
identifier: string | null;
|
||||
title: string | null;
|
||||
} | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "budget";
|
||||
observedPercent: number;
|
||||
amountObserved: number;
|
||||
amountLimit: number;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "agent_error";
|
||||
agentName: string | null;
|
||||
failureReasonExcerpt: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
}
|
||||
| {
|
||||
kind: "generic";
|
||||
summaryExcerpt: string | null;
|
||||
images: AttentionDetailImage[];
|
||||
};
|
||||
|
||||
export interface AttentionItem {
|
||||
id: string;
|
||||
companyId: string;
|
||||
sourceKind: AttentionSourceKind;
|
||||
subject: AttentionSubject;
|
||||
whyNow: string;
|
||||
decisionVerbs: AttentionDecisionVerb[];
|
||||
inlineResolvable: boolean;
|
||||
entryRule: string;
|
||||
exitRule: string;
|
||||
dedupKey: string;
|
||||
dismissalKey: string;
|
||||
dismissal: AttentionItemDismissal | null;
|
||||
severity: AttentionSeverity;
|
||||
rank: number;
|
||||
activityAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
relatedIssue: AttentionSubject | null;
|
||||
project: AttentionProjectRef | null;
|
||||
workspace: AttentionWorkspaceRef | null;
|
||||
detail: AttentionItemDetail | null;
|
||||
}
|
||||
|
||||
export interface AttentionFeed {
|
||||
companyId: string;
|
||||
generatedAt: string;
|
||||
totalCount: number;
|
||||
countsBySourceKind: Record<AttentionSourceKind, number>;
|
||||
items: AttentionItem[];
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
export type InboxDismissalKind = "dismiss" | "snooze";
|
||||
|
||||
export interface InboxDismissal {
|
||||
id: string;
|
||||
companyId: string;
|
||||
userId: string;
|
||||
itemKey: string;
|
||||
kind: InboxDismissalKind;
|
||||
dismissedAt: Date;
|
||||
snoozedUntil: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,4 +1,18 @@
|
||||
export type { Company } from "./company.js";
|
||||
export type {
|
||||
AttentionDecisionVerb,
|
||||
AttentionDetailImage,
|
||||
AttentionFeed,
|
||||
AttentionItem,
|
||||
AttentionItemDetail,
|
||||
AttentionItemDismissal,
|
||||
AttentionProjectRef,
|
||||
AttentionSeverity,
|
||||
AttentionSourceKind,
|
||||
AttentionSubject,
|
||||
AttentionSubjectKind,
|
||||
AttentionWorkspaceRef,
|
||||
} from "./attention.js";
|
||||
export type {
|
||||
Environment,
|
||||
EnvironmentDeleteBlastRadius,
|
||||
@@ -400,6 +414,11 @@ export type {
|
||||
RequestCheckboxConfirmationOption,
|
||||
RequestCheckboxConfirmationPayload,
|
||||
RequestCheckboxConfirmationResult,
|
||||
RequestItemVerdictValue,
|
||||
RequestItemVerdictsItem,
|
||||
RequestItemVerdictsPayload,
|
||||
RequestItemVerdictsResultItem,
|
||||
RequestItemVerdictsResult,
|
||||
AcceptedPlanDecompositionStatus,
|
||||
AcceptedPlanDecompositionChild,
|
||||
AcceptedPlanDecomposition,
|
||||
@@ -411,6 +430,7 @@ export type {
|
||||
AskUserQuestionsInteraction,
|
||||
RequestConfirmationInteraction,
|
||||
RequestCheckboxConfirmationInteraction,
|
||||
RequestItemVerdictsInteraction,
|
||||
IssueThreadInteraction,
|
||||
IssueThreadInteractionPayload,
|
||||
IssueThreadInteractionResult,
|
||||
@@ -558,7 +578,7 @@ export type {
|
||||
UpdateResourceMembership,
|
||||
} from "./resource-memberships.js";
|
||||
export { RESOURCE_MEMBERSHIP_STATES } from "./resource-memberships.js";
|
||||
export type { InboxDismissal } from "./inbox-dismissal.js";
|
||||
export type { InboxDismissal, InboxDismissalKind } from "./inbox-dismissal.js";
|
||||
export type {
|
||||
AccessUserProfile,
|
||||
CompanyMemberRecord,
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface InstanceExperimentalSettings {
|
||||
enableCloudSync: boolean;
|
||||
enableExternalObjects: boolean;
|
||||
enableBuiltInAgents: boolean;
|
||||
enableDecisions: boolean;
|
||||
enableGoalsSidebarLink: boolean;
|
||||
enableServerInfoDebugView: boolean;
|
||||
autoRestartDevServerWhenIdle: boolean;
|
||||
|
||||
@@ -1082,6 +1082,30 @@ export interface RequestCheckboxConfirmationPayload {
|
||||
target?: RequestConfirmationTarget | null;
|
||||
}
|
||||
|
||||
export type RequestItemVerdictValue = "approve" | "reject" | "defer";
|
||||
|
||||
export interface RequestItemVerdictsItem {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
previewMarkdown?: string | null;
|
||||
href?: string | null;
|
||||
attachmentId?: string | null;
|
||||
}
|
||||
|
||||
export interface RequestItemVerdictsPayload {
|
||||
version: 1;
|
||||
prompt: string;
|
||||
detailsMarkdown?: string | null;
|
||||
items: RequestItemVerdictsItem[];
|
||||
verdicts?: RequestItemVerdictValue[];
|
||||
requireReasonOn?: RequestItemVerdictValue[];
|
||||
reasonLabel?: string | null;
|
||||
allowBulkApprove?: boolean;
|
||||
supersedeOnUserComment?: boolean;
|
||||
target?: RequestConfirmationTarget | null;
|
||||
}
|
||||
|
||||
export interface RequestConfirmationResult {
|
||||
version: 1;
|
||||
outcome: "accepted" | "rejected" | "superseded_by_comment" | "stale_target";
|
||||
@@ -1104,6 +1128,24 @@ export interface RequestCheckboxConfirmationResult extends RequestConfirmationRe
|
||||
selectedOptionIds?: string[];
|
||||
}
|
||||
|
||||
export interface RequestItemVerdictsResultItem {
|
||||
id: string;
|
||||
verdict: RequestItemVerdictValue;
|
||||
reason?: string | null;
|
||||
resolvedByUserId: string;
|
||||
resolvedAt: Date | string;
|
||||
commentId?: string | null;
|
||||
}
|
||||
|
||||
export interface RequestItemVerdictsResult {
|
||||
version: 1;
|
||||
outcome: "resolved" | "superseded_by_comment" | "stale_target" | "cancelled";
|
||||
complete: boolean;
|
||||
items: RequestItemVerdictsResultItem[];
|
||||
commentId?: string | null;
|
||||
staleTarget?: RequestConfirmationTarget | null;
|
||||
}
|
||||
|
||||
export interface IssueThreadInteractionBase extends IssueThreadInteractionActorFields {
|
||||
id: string;
|
||||
companyId: string;
|
||||
@@ -1145,23 +1187,32 @@ export interface RequestCheckboxConfirmationInteraction extends IssueThreadInter
|
||||
result?: RequestCheckboxConfirmationResult | null;
|
||||
}
|
||||
|
||||
export interface RequestItemVerdictsInteraction extends IssueThreadInteractionBase {
|
||||
kind: "request_item_verdicts";
|
||||
payload: RequestItemVerdictsPayload;
|
||||
result?: RequestItemVerdictsResult | null;
|
||||
}
|
||||
|
||||
export type IssueThreadInteraction =
|
||||
| SuggestTasksInteraction
|
||||
| AskUserQuestionsInteraction
|
||||
| RequestConfirmationInteraction
|
||||
| RequestCheckboxConfirmationInteraction;
|
||||
| RequestCheckboxConfirmationInteraction
|
||||
| RequestItemVerdictsInteraction;
|
||||
|
||||
export type IssueThreadInteractionPayload =
|
||||
| SuggestTasksPayload
|
||||
| AskUserQuestionsPayload
|
||||
| RequestConfirmationPayload
|
||||
| RequestCheckboxConfirmationPayload;
|
||||
| RequestCheckboxConfirmationPayload
|
||||
| RequestItemVerdictsPayload;
|
||||
|
||||
export type IssueThreadInteractionResult =
|
||||
| SuggestTasksResult
|
||||
| AskUserQuestionsResult
|
||||
| RequestConfirmationResult
|
||||
| RequestCheckboxConfirmationResult;
|
||||
| RequestCheckboxConfirmationResult
|
||||
| RequestItemVerdictsResult;
|
||||
|
||||
export interface IssueAttachment {
|
||||
id: string;
|
||||
|
||||
@@ -364,11 +364,17 @@ export {
|
||||
requestCheckboxConfirmationOptionSchema,
|
||||
requestCheckboxConfirmationPayloadSchema,
|
||||
requestCheckboxConfirmationResultSchema,
|
||||
requestItemVerdictValueSchema,
|
||||
requestItemVerdictsItemSchema,
|
||||
requestItemVerdictsPayloadSchema,
|
||||
requestItemVerdictsResultItemSchema,
|
||||
requestItemVerdictsResultSchema,
|
||||
createIssueThreadInteractionSchema,
|
||||
acceptIssueThreadInteractionSchema,
|
||||
rejectIssueThreadInteractionSchema,
|
||||
cancelIssueThreadInteractionSchema,
|
||||
respondIssueThreadInteractionSchema,
|
||||
submitIssueThreadInteractionVerdictsSchema,
|
||||
linkIssueApprovalSchema,
|
||||
createIssueAttachmentMetadataSchema,
|
||||
issueDocumentFormatSchema,
|
||||
@@ -391,6 +397,7 @@ export {
|
||||
type RejectIssueThreadInteraction,
|
||||
type CancelIssueThreadInteraction,
|
||||
type RespondIssueThreadInteraction,
|
||||
type SubmitIssueThreadInteractionVerdicts,
|
||||
type LinkIssueApproval,
|
||||
type CreateIssueAttachmentMetadata,
|
||||
type IssueDocumentFormat,
|
||||
|
||||
@@ -60,6 +60,22 @@ describe("instance experimental settings validators", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults the decisions sidebar link off", () => {
|
||||
const settings = instanceExperimentalSettingsSchema.parse({});
|
||||
|
||||
expect(settings.enableDecisions).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts decisions patches", () => {
|
||||
expect(
|
||||
patchInstanceExperimentalSettingsSchema.parse({
|
||||
enableDecisions: true,
|
||||
}),
|
||||
).toEqual({
|
||||
enableDecisions: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts server info debug view patches", () => {
|
||||
expect(
|
||||
patchInstanceExperimentalSettingsSchema.parse({
|
||||
|
||||
@@ -51,6 +51,7 @@ export const instanceExperimentalSettingsSchema = z.object({
|
||||
enableCloudSync: z.boolean().default(false),
|
||||
enableExternalObjects: z.boolean().default(false),
|
||||
enableBuiltInAgents: z.boolean().default(false),
|
||||
enableDecisions: z.boolean().default(false),
|
||||
enableGoalsSidebarLink: z.boolean().default(false),
|
||||
enableServerInfoDebugView: z.boolean().default(false),
|
||||
autoRestartDevServerWhenIdle: z.boolean().default(false),
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
ISSUE_WATCHDOG_DISCOVERY_KINDS,
|
||||
MODEL_PROFILE_KEYS,
|
||||
REQUEST_CHECKBOX_CONFIRMATION_OPTION_LIMIT,
|
||||
REQUEST_ITEM_VERDICTS_ITEM_LIMIT,
|
||||
} from "../constants.js";
|
||||
import { multilineTextSchema } from "./text.js";
|
||||
import { lowTrustReviewPresetPolicySchema, trustAuthorizationPolicySchema } from "./trust-policy.js";
|
||||
@@ -894,6 +895,121 @@ export const requestCheckboxConfirmationResultSchema = requestConfirmationResult
|
||||
}
|
||||
});
|
||||
|
||||
export const requestItemVerdictValueSchema = z.enum(["approve", "reject", "defer"]);
|
||||
|
||||
export const requestItemVerdictsItemSchema = z.object({
|
||||
id: z.string().trim().min(1).max(120),
|
||||
label: z.string().trim().min(1).max(120),
|
||||
description: z.string().trim().max(500).nullable().optional(),
|
||||
previewMarkdown: z.string().max(20000).nullable().optional(),
|
||||
href: requestConfirmationHrefSchema.nullable().optional(),
|
||||
attachmentId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export const requestItemVerdictsPayloadSchema = z.object({
|
||||
version: z.literal(1),
|
||||
prompt: z.string().trim().min(1).max(1000),
|
||||
detailsMarkdown: z.string().max(20000).nullable().optional(),
|
||||
items: z.array(requestItemVerdictsItemSchema)
|
||||
.min(1)
|
||||
.max(REQUEST_ITEM_VERDICTS_ITEM_LIMIT),
|
||||
verdicts: z.array(requestItemVerdictValueSchema)
|
||||
.min(2)
|
||||
.max(3)
|
||||
.optional()
|
||||
.default(["approve", "reject"]),
|
||||
requireReasonOn: z.array(requestItemVerdictValueSchema)
|
||||
.max(3)
|
||||
.optional()
|
||||
.default(["reject"]),
|
||||
reasonLabel: z.string().trim().min(1).max(160).nullable().optional(),
|
||||
allowBulkApprove: z.boolean().optional().default(true),
|
||||
supersedeOnUserComment: z.boolean().optional(),
|
||||
target: requestConfirmationTargetSchema.nullable().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const itemIds = new Set<string>();
|
||||
for (const [index, item] of value.items.entries()) {
|
||||
if (itemIds.has(item.id)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Item ids must be unique within one item verdict request",
|
||||
path: ["items", index, "id"],
|
||||
});
|
||||
}
|
||||
itemIds.add(item.id);
|
||||
}
|
||||
|
||||
const verdicts = new Set<string>();
|
||||
for (const [index, verdict] of value.verdicts.entries()) {
|
||||
if (verdicts.has(verdict)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "verdicts must be unique",
|
||||
path: ["verdicts", index],
|
||||
});
|
||||
}
|
||||
verdicts.add(verdict);
|
||||
}
|
||||
if (!verdicts.has("approve") || !verdicts.has("reject")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "verdicts must include approve and reject; defer is optional",
|
||||
path: ["verdicts"],
|
||||
});
|
||||
}
|
||||
|
||||
const reasonVerdicts = new Set<string>();
|
||||
for (const [index, verdict] of value.requireReasonOn.entries()) {
|
||||
if (reasonVerdicts.has(verdict)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "requireReasonOn must be unique",
|
||||
path: ["requireReasonOn", index],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
reasonVerdicts.add(verdict);
|
||||
if (!verdicts.has(verdict)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "requireReasonOn must reference enabled verdicts",
|
||||
path: ["requireReasonOn", index],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const requestItemVerdictsResultItemSchema = z.object({
|
||||
id: z.string().trim().min(1).max(120),
|
||||
verdict: requestItemVerdictValueSchema,
|
||||
reason: z.string().trim().max(4000).nullable().optional(),
|
||||
resolvedByUserId: z.string().trim().min(1).max(255),
|
||||
resolvedAt: z.union([z.string().datetime(), z.date()]),
|
||||
commentId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export const requestItemVerdictsResultSchema = z.object({
|
||||
version: z.literal(1),
|
||||
outcome: z.enum(["resolved", "superseded_by_comment", "stale_target", "cancelled"]),
|
||||
complete: z.boolean(),
|
||||
items: z.array(requestItemVerdictsResultItemSchema)
|
||||
.max(REQUEST_ITEM_VERDICTS_ITEM_LIMIT),
|
||||
commentId: z.string().uuid().nullable().optional(),
|
||||
staleTarget: requestConfirmationTargetSchema.nullable().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const itemIds = new Set<string>();
|
||||
for (const [index, item] of value.items.entries()) {
|
||||
if (itemIds.has(item.id)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "result item ids must be unique",
|
||||
path: ["items", index, "id"],
|
||||
});
|
||||
}
|
||||
itemIds.add(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [
|
||||
z.object({
|
||||
kind: z.literal("suggest_tasks"),
|
||||
@@ -935,6 +1051,16 @@ export const createIssueThreadInteractionSchema = z.discriminatedUnion("kind", [
|
||||
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
|
||||
payload: requestCheckboxConfirmationPayloadSchema,
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal("request_item_verdicts"),
|
||||
idempotencyKey: z.string().trim().max(255).nullable().optional(),
|
||||
sourceCommentId: z.string().uuid().nullable().optional(),
|
||||
sourceRunId: z.string().uuid().nullable().optional(),
|
||||
title: z.string().trim().max(240).nullable().optional(),
|
||||
summary: z.string().trim().max(1000).nullable().optional(),
|
||||
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
|
||||
payload: requestItemVerdictsPayloadSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type CreateIssueThreadInteraction = z.infer<typeof createIssueThreadInteractionSchema>;
|
||||
@@ -989,6 +1115,29 @@ export const respondIssueThreadInteractionSchema = z.object({
|
||||
});
|
||||
export type RespondIssueThreadInteraction = z.infer<typeof respondIssueThreadInteractionSchema>;
|
||||
|
||||
export const submitIssueThreadInteractionVerdictsSchema = z.object({
|
||||
verdicts: z.array(z.object({
|
||||
id: z.string().trim().min(1).max(120),
|
||||
verdict: requestItemVerdictValueSchema,
|
||||
reason: z.string().trim().max(4000).nullable().optional(),
|
||||
}))
|
||||
.min(1)
|
||||
.max(REQUEST_ITEM_VERDICTS_ITEM_LIMIT),
|
||||
}).superRefine((value, ctx) => {
|
||||
const itemIds = new Set<string>();
|
||||
for (const [index, verdict] of value.verdicts.entries()) {
|
||||
if (itemIds.has(verdict.id)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "verdict item ids must be unique",
|
||||
path: ["verdicts", index, "id"],
|
||||
});
|
||||
}
|
||||
itemIds.add(verdict.id);
|
||||
}
|
||||
});
|
||||
export type SubmitIssueThreadInteractionVerdicts = z.infer<typeof submitIssueThreadInteractionVerdictsSchema>;
|
||||
|
||||
export const linkIssueApprovalSchema = z.object({
|
||||
approvalId: z.string().uuid(),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user