Add first-class resolved suggestion status + doc-review P3 polish (PAP-10574)

Deferred polish from the PAP-10568 UX review.

First-class `resolved` suggestion status (backend):
- Add `resolved` to DOCUMENT_SUGGESTION_STATUSES + resolved-by/at audit
  columns (schema + migration 0101) and a `resolvedSuggestions` count.
- New POST .../suggestions/:id/resolve endpoint + service method that sets
  status without editing the document, distinct from reject (disagreement).
- Wire the rail's Resolve action to the new endpoint; show a Resolved badge.

Mobile FAB safe-area inset (§5.8): lift the review FAB above the iOS home
indicator with bottom-[max(env(safe-area-inset-bottom),1rem)].

Platform-aware hotkey hint: RailEmpty shows Ctrl+Shift+M on Windows/Linux
and ⌘⇧M on macOS/iOS via a new isApplePlatform() helper.

(Autosave indicator wiring is left for when document autosave lands — the
header slot already exists.)

Tests: resolveSuggestion service test (terminal status, no revision created),
SuggestionCard resolved-badge test, platform-aware empty-state hint test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-06-07 02:47:20 +00:00
1 parent 2730223160
commit 0f0bb8e107
21 files changed
+271 -10

No files matched your search

@@ -0,0 +1,12 @@
-- First-class `resolved` suggestion status (PAP-10574). "Resolved" = handled
-- outside review / no longer applies, distinct from "rejected" (disagreement).
-- `status` is a plain text column with no enum/check constraint, so the new
-- value needs no constraint change — only the audit columns below.
ALTER TABLE "document_suggestions" ADD COLUMN IF NOT EXISTS "resolved_by_agent_id" uuid;--> statement-breakpoint
ALTER TABLE "document_suggestions" ADD COLUMN IF NOT EXISTS "resolved_by_user_id" text;--> statement-breakpoint
ALTER TABLE "document_suggestions" ADD COLUMN IF NOT EXISTS "resolved_at" timestamp with time zone;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "document_suggestions" ADD CONSTRAINT "document_suggestions_resolved_by_agent_id_agents_id_fk" FOREIGN KEY ("resolved_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
@@ -708,6 +708,13 @@
"when": 1780794300000,
"tag": "0100_document_review_backend",
"breakpoints": true
},
{
"idx": 101,
"version": "7",
"when": 1780794600000,
"tag": "0101_document_suggestion_resolved_status",
"breakpoints": true
}
]
}
@@ -48,6 +48,9 @@ export const documentSuggestions = pgTable(
rejectedByAgentId: uuid("rejected_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
rejectedByUserId: text("rejected_by_user_id"),
rejectedAt: timestamp("rejected_at", { withTimezone: true }),
resolvedByAgentId: uuid("resolved_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
resolvedByUserId: text("resolved_by_user_id"),
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
+3 -1
View File
@@ -360,7 +360,9 @@ export type DocumentSuggestionKind = (typeof DOCUMENT_SUGGESTION_KINDS)[number];
export const DOCUMENT_SUGGESTION_INSERT_POSITIONS = ["before", "after"] as const;
export type DocumentSuggestionInsertPosition = (typeof DOCUMENT_SUGGESTION_INSERT_POSITIONS)[number];
export const DOCUMENT_SUGGESTION_STATUSES = ["pending", "accepted", "rejected"] as const;
// `resolved` = handled outside review / no longer applies — distinct from
// `rejected` (an explicit disagreement) so the audit trail keeps the two apart.
export const DOCUMENT_SUGGESTION_STATUSES = ["pending", "accepted", "rejected", "resolved"] as const;
export type DocumentSuggestionStatus = (typeof DOCUMENT_SUGGESTION_STATUSES)[number];
export const DOCUMENT_STATUSES = ["draft", "in_review", "approved", "archived"] as const;
+3
View File
@@ -522,6 +522,7 @@ export type {
DocumentSuggestionComment,
DocumentSuggestionWithComments,
RejectDocumentSuggestionRequest,
ResolveDocumentSuggestionRequest,
UpdateDocumentAnnotationThreadRequest,
UpdateDocumentReviewThreadRequest,
AttachmentArtifactWorkProductMetadata,
@@ -1011,6 +1012,7 @@ export {
documentSuggestionKindSchema,
documentSuggestionStatusSchema,
rejectDocumentSuggestionSchema,
resolveDocumentSuggestionSchema,
updateDocumentReviewThreadSchema,
type AcceptDocumentSuggestion,
type CreateDocumentReviewComment,
@@ -1018,6 +1020,7 @@ export {
type CreateDocumentSuggestion,
type CreateDocumentSuggestionComment,
type RejectDocumentSuggestion,
type ResolveDocumentSuggestion,
type UpdateDocumentReviewThread,
companySearchQuerySchema,
COMPANY_SEARCH_DEFAULT_LIMIT,
@@ -83,6 +83,9 @@ export interface DocumentSuggestion {
rejectedByAgentId: string | null;
rejectedByUserId: string | null;
rejectedAt: Date | null;
resolvedByAgentId: string | null;
resolvedByUserId: string | null;
resolvedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
@@ -133,6 +136,7 @@ export interface DocumentReviewIndexCounts {
resolvedReviewThreads: number;
acceptedSuggestions: number;
rejectedSuggestions: number;
resolvedSuggestions: number;
staleAnchors: number;
orphanedAnchors: number;
}
@@ -187,3 +191,8 @@ export interface AcceptDocumentSuggestionRequest {
export interface RejectDocumentSuggestionRequest {
reason?: string | null;
}
export interface ResolveDocumentSuggestionRequest {
/** Optional note kept for the audit trail (e.g. where it was handled). */
note?: string | null;
}
+1
View File
@@ -191,6 +191,7 @@ export type {
DocumentSuggestionComment,
DocumentSuggestionWithComments,
RejectDocumentSuggestionRequest,
ResolveDocumentSuggestionRequest,
UpdateDocumentReviewThreadRequest,
} from "./document-review.js";
export type { Project, ProjectBudgetSummary, ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, ProjectManagedByPlugin, ProjectWorkspace } from "./project.js";
@@ -80,6 +80,10 @@ export const rejectDocumentSuggestionSchema = z.object({
reason: z.string().trim().min(1).max(2_000).nullable().optional(),
}).strict();
export const resolveDocumentSuggestionSchema = z.object({
note: z.string().trim().min(1).max(2_000).nullable().optional(),
}).strict();
export type CreateDocumentReviewThread = z.infer<typeof createDocumentReviewThreadSchema>;
export type CreateDocumentReviewComment = z.infer<typeof createDocumentReviewCommentSchema>;
export type UpdateDocumentReviewThread = z.infer<typeof updateDocumentReviewThreadSchema>;
@@ -87,3 +91,4 @@ export type CreateDocumentSuggestion = z.infer<typeof createDocumentSuggestionSc
export type CreateDocumentSuggestionComment = z.infer<typeof createDocumentSuggestionCommentSchema>;
export type AcceptDocumentSuggestion = z.infer<typeof acceptDocumentSuggestionSchema>;
export type RejectDocumentSuggestion = z.infer<typeof rejectDocumentSuggestionSchema>;
export type ResolveDocumentSuggestion = z.infer<typeof resolveDocumentSuggestionSchema>;
+2
View File
@@ -240,6 +240,7 @@ export {
documentSuggestionKindSchema,
documentSuggestionStatusSchema,
rejectDocumentSuggestionSchema,
resolveDocumentSuggestionSchema,
updateDocumentReviewThreadSchema,
type AcceptDocumentSuggestion,
type CreateDocumentReviewComment,
@@ -247,6 +248,7 @@ export {
type CreateDocumentSuggestion,
type CreateDocumentSuggestionComment,
type RejectDocumentSuggestion,
type ResolveDocumentSuggestion,
type UpdateDocumentReviewThread,
} from "./document-review.js";
@@ -170,6 +170,47 @@ describeEmbeddedPostgres("documentReviewService", () => {
expect(index.counts.acceptedSuggestions).toBe(1);
});
it("resolves a suggestion without editing the document, distinct from rejection", async () => {
const { issueId, document } = await createIssueWithDocument();
const suggestion = await review.createSuggestion(
issueId,
"plan",
{
baseRevisionId: document.latestRevisionId!,
baseRevisionNumber: document.latestRevisionNumber,
kind: "substitution",
selector: selectedTextSelector,
proposedText: "better text",
},
{ actorType: "user", actorId: "board-user", userId: "board-user" },
);
const resolved = await review.resolveSuggestion(
issueId,
"plan",
suggestion.id,
{ note: "Handled in a follow-up edit." },
{ actorType: "user", actorId: "board-user", userId: "board-user" },
);
expect(resolved.status).toBe("resolved");
expect(resolved.resolvedByUserId).toBe("board-user");
expect(resolved.resolvedAt).toBeInstanceOf(Date);
// Resolving must not mutate the document body or create a revision.
const revisions = await docs.listIssueDocumentRevisions(issueId, "plan");
expect(revisions).toHaveLength(1);
const index = await review.getReviewIndex(issueId, "plan", { status: "all" });
expect(index.counts.pendingSuggestions).toBe(0);
expect(index.counts.resolvedSuggestions).toBe(1);
expect(index.counts.rejectedSuggestions).toBe(0);
// A resolved suggestion is terminal — it can't be resolved or rejected again.
await expect(
review.resolveSuggestion(issueId, "plan", suggestion.id, {}, { actorType: "user", actorId: "board-user", userId: "board-user" }),
).rejects.toThrow(/Only pending suggestions can be resolved/);
});
it("remaps pending suggestions across document revisions before acceptance", async () => {
const { issueId, document } = await createIssueWithDocument();
const suggestion = await review.createSuggestion(
+47
View File
@@ -49,6 +49,7 @@ import {
issueDocumentKeySchema,
ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY,
rejectDocumentSuggestionSchema,
resolveDocumentSuggestionSchema,
rejectIssueThreadInteractionSchema,
restoreIssueDocumentRevisionSchema,
respondIssueThreadInteractionSchema,
@@ -3579,6 +3580,52 @@ export function issueRoutes(
},
);
router.post(
"/issues/:id/documents/:key/suggestions/:suggestionId/resolve",
validate(resolveDocumentSuggestionSchema),
async (req, res) => {
const id = req.params.id as string;
const issue = await svc.getById(id);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
}
assertCompanyAccess(req, issue.companyId);
if (!(await assertAgentIssueMutationAllowed(req, res, issue))) return;
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
if (!keyParsed.success) {
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
return;
}
const { actor, annotationActor } = annotationActorInput(req);
const suggestion = await documentReviewsSvc.resolveSuggestion(
issue.id,
keyParsed.data,
req.params.suggestionId as string,
req.body,
annotationActor,
);
await logActivity(db, {
companyId: issue.companyId,
actorType: actor.actorType,
actorId: actor.actorId,
agentId: actor.agentId,
runId: actor.runId,
action: "issue.document_suggestion_resolved",
entityType: "issue",
entityId: issue.id,
details: {
documentKey: suggestion.documentKey,
documentId: suggestion.documentId,
suggestionId: suggestion.id,
kind: suggestion.kind,
note: req.body.note ?? null,
},
});
res.json(suggestion);
},
);
router.post(
"/issues/:id/documents/:key/suggestions/:suggestionId/accept",
validate(acceptDocumentSuggestionSchema),
+33
View File
@@ -32,6 +32,7 @@ import {
type DocumentSuggestion,
type DocumentSuggestionComment,
type RejectDocumentSuggestion,
type ResolveDocumentSuggestion,
type UpdateDocumentReviewThread,
} from "@paperclipai/shared";
import { conflict, notFound, unprocessable } from "../errors.js";
@@ -167,6 +168,9 @@ const suggestionSelect = {
rejectedByAgentId: documentSuggestions.rejectedByAgentId,
rejectedByUserId: documentSuggestions.rejectedByUserId,
rejectedAt: documentSuggestions.rejectedAt,
resolvedByAgentId: documentSuggestions.resolvedByAgentId,
resolvedByUserId: documentSuggestions.resolvedByUserId,
resolvedAt: documentSuggestions.resolvedAt,
createdAt: documentSuggestions.createdAt,
updatedAt: documentSuggestions.updatedAt,
};
@@ -365,6 +369,7 @@ export function documentReviewService(db: Db) {
pendingSuggestions: sql<number>`count(*) filter (where ${documentSuggestions.status} = 'pending')`.mapWith(Number),
acceptedSuggestions: sql<number>`count(*) filter (where ${documentSuggestions.status} = 'accepted')`.mapWith(Number),
rejectedSuggestions: sql<number>`count(*) filter (where ${documentSuggestions.status} = 'rejected')`.mapWith(Number),
resolvedSuggestions: sql<number>`count(*) filter (where ${documentSuggestions.status} = 'resolved')`.mapWith(Number),
staleAnchors: sql<number>`count(*) filter (where ${documentSuggestions.status} = 'pending' and ${documentSuggestions.anchorState} = 'stale')`.mapWith(Number),
orphanedAnchors: sql<number>`count(*) filter (where ${documentSuggestions.status} = 'pending' and ${documentSuggestions.anchorState} = 'orphaned')`.mapWith(Number),
}).from(documentSuggestions).where(and(
@@ -387,6 +392,7 @@ export function documentReviewService(db: Db) {
pendingSuggestions: 0,
acceptedSuggestions: 0,
rejectedSuggestions: 0,
resolvedSuggestions: 0,
staleAnchors: 0,
orphanedAnchors: 0,
};
@@ -398,6 +404,7 @@ export function documentReviewService(db: Db) {
pendingSuggestions: suggestionCounts.pendingSuggestions,
acceptedSuggestions: suggestionCounts.acceptedSuggestions,
rejectedSuggestions: suggestionCounts.rejectedSuggestions,
resolvedSuggestions: suggestionCounts.resolvedSuggestions,
staleAnchors: annotationCounts.staleAnchors + suggestionCounts.staleAnchors,
orphanedAnchors: annotationCounts.orphanedAnchors + suggestionCounts.orphanedAnchors,
};
@@ -675,6 +682,32 @@ export function documentReviewService(db: Db) {
return updated;
}),
// "Resolve" = handled outside review / no longer applies. Distinct from
// reject (disagreement) so the audit trail keeps the two apart. Like reject,
// only a pending suggestion can be resolved.
resolveSuggestion: async (
issueId: string,
key: string,
suggestionId: string,
_input: ResolveDocumentSuggestion,
actor: ActorInput,
) => db.transaction(async (tx) => {
const suggestion = await getSuggestionForIssue(issueId, key, suggestionId, tx);
if (!suggestion) throw notFound("Suggestion not found");
if (suggestion.status !== "pending") {
throw conflict("Only pending suggestions can be resolved", { status: suggestion.status });
}
const now = new Date();
const [updated] = await tx.update(documentSuggestions).set({
status: "resolved",
resolvedByAgentId: actor.agentId ?? null,
resolvedByUserId: actor.userId ?? null,
resolvedAt: now,
updatedAt: now,
}).where(eq(documentSuggestions.id, suggestion.id)).returning(suggestionSelect);
return updated;
}),
acceptSuggestion: async (
issueId: string,
key: string,
+13
View File
@@ -12,6 +12,7 @@ import type {
DocumentSuggestion,
DocumentSuggestionComment,
RejectDocumentSuggestionRequest,
ResolveDocumentSuggestionRequest,
} from "@paperclipai/shared";
import { api } from "./client";
@@ -112,4 +113,16 @@ export const documentReviewsApi = {
`/issues/${issueId}/documents/${encodeURIComponent(key)}/suggestions/${suggestionId}/reject`,
data,
),
// "Resolve" = handled outside review / no longer applies — a first-class status
// distinct from reject (disagreement) so the audit trail keeps the two apart.
resolveSuggestion: (
issueId: string,
key: string,
suggestionId: string,
data: ResolveDocumentSuggestionRequest = {},
) =>
api.post<DocumentSuggestion>(
`/issues/${issueId}/documents/${encodeURIComponent(key)}/suggestions/${suggestionId}/resolve`,
data,
),
};
@@ -72,6 +72,9 @@ function makeSuggestion(overrides: Partial<DocumentSuggestionWithComments> = {})
rejectedByAgentId: null,
rejectedByUserId: null,
rejectedAt: null,
resolvedByAgentId: null,
resolvedByUserId: null,
resolvedAt: null,
createdAt: new Date("2026-06-01T00:00:00Z"),
updatedAt: new Date("2026-06-01T00:00:00Z"),
comments: [],
@@ -95,6 +98,7 @@ function makeIndex(overrides: Partial<DocumentReviewIndex> = {}): DocumentReview
resolvedReviewThreads: 0,
acceptedSuggestions: 0,
rejectedSuggestions: 0,
resolvedSuggestions: 0,
staleAnchors: 0,
orphanedAnchors: 0,
},
@@ -291,6 +295,36 @@ describe("DocumentReviewRail", () => {
expect(onDoneReviewing).not.toHaveBeenCalled();
});
it("shows a platform-aware comment hotkey in the empty state", async () => {
const emptyIndex = makeIndex({
counts: { ...makeIndex().counts, openAnchoredThreads: 0, openReviewThreads: 0, pendingSuggestions: 0 },
annotationThreads: [],
reviewThreads: [],
suggestions: [],
});
const original = Object.getOwnPropertyDescriptor(window.navigator, "platform");
// Windows/Linux reviewers should see the Ctrl spelling…
Object.defineProperty(window.navigator, "platform", { value: "Win32", configurable: true });
await act(() =>
root.render(
<DocumentReviewRail reviewIndex={emptyIndex} canReview canFinishReview latestRevisionId="rev-1" {...baseHandlers} />,
),
);
expect(container.querySelector('[data-testid="rail-empty"]')?.textContent).toContain("Ctrl+Shift+M");
// …and macOS reviewers the ⌘ spelling.
Object.defineProperty(window.navigator, "platform", { value: "MacIntel", configurable: true });
await act(() =>
root.render(
<DocumentReviewRail reviewIndex={emptyIndex} canReview canFinishReview latestRevisionId="rev-1" {...baseHandlers} />,
),
);
expect(container.querySelector('[data-testid="rail-empty"]')?.textContent).toContain("⌘⇧M");
if (original) Object.defineProperty(window.navigator, "platform", original);
});
it("fires the Done reviewing handler when the viewer can finish review", async () => {
const onDoneReviewing = vi.fn();
await act(() =>
@@ -41,6 +41,7 @@ function counts(overrides: Partial<DocumentReviewIndexCounts> = {}): DocumentRev
resolvedReviewThreads: 0,
acceptedSuggestions: 0,
rejectedSuggestions: 0,
resolvedSuggestions: 0,
staleAnchors: 1,
orphanedAnchors: 4,
...overrides,
@@ -69,6 +69,9 @@ function makeSuggestion(overrides: Partial<DocumentSuggestionWithComments> = {})
rejectedByAgentId: null,
rejectedByUserId: null,
rejectedAt: null,
resolvedByAgentId: null,
resolvedByUserId: null,
resolvedAt: null,
createdAt: new Date("2026-06-01T00:00:00Z"),
updatedAt: new Date("2026-06-01T00:00:00Z"),
comments: [],
@@ -198,6 +201,27 @@ describe("SuggestionCard", () => {
expect(container.querySelector('[data-testid="suggestion-resolve-sug-1"]')).toBeNull();
});
it("shows a Resolved badge and no actions for a resolved suggestion", async () => {
await act(() =>
root.render(
<SuggestionCard
suggestion={makeSuggestion({ status: "resolved", resolvedAt: new Date() })}
latestRevisionId="rev-1"
canReview
onAccept={vi.fn()}
onReject={vi.fn()}
onResolve={vi.fn()}
onReply={vi.fn()}
/>,
),
);
expect(container.querySelector('[data-testid="suggestion-resolved-sug-1"]')).not.toBeNull();
expect(container.textContent).toContain("Resolved");
// Terminal status: accept/reject/resolve actions are gone.
expect(container.querySelector('[data-testid="suggestion-accept-sug-1"]')).toBeNull();
expect(container.querySelector('[data-testid="suggestion-resolve-sug-1"]')).toBeNull();
});
it("hides review actions for read-only viewers", async () => {
await act(() =>
root.render(
+10 -1
View File
@@ -150,7 +150,7 @@ export function SuggestionCard({
data-status={suggestion.status}
className={cn(
"rounded-md border border-border bg-card p-2.5 text-card-foreground shadow-sm",
suggestion.status === "rejected" && "opacity-70",
(suggestion.status === "rejected" || suggestion.status === "resolved") && "opacity-70",
className,
)}
>
@@ -186,6 +186,15 @@ export function SuggestionCard({
Rejected
</Badge>
) : null}
{suggestion.status === "resolved" ? (
<Badge
data-testid={`suggestion-resolved-${suggestion.id}`}
className="gap-1 border-transparent bg-muted px-1.5 py-0 text-[10px] text-muted-foreground"
>
<CheckCheck className="h-3 w-3" aria-hidden="true" />
Resolved
</Badge>
) : null}
</div>
{onViewDiff ? (
<Button
+12
View File
@@ -7,6 +7,18 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Best-effort detection of an Apple platform (macOS / iOS) so hotkey hints can
* show `⌘` instead of `Ctrl`. Prefers the modern UA-Client-Hints `platform`
* field and falls back to `navigator.platform`. SSR-safe (returns false).
*/
export function isApplePlatform(): boolean {
if (typeof navigator === "undefined") return false;
const uaPlatform = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform;
const platform = uaPlatform || navigator.platform || "";
return /mac|iphone|ipad|ipod/i.test(platform);
}
export function asObject(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
+1
View File
@@ -137,6 +137,7 @@ const emptyIndex = {
resolvedReviewThreads: 0,
acceptedSuggestions: 0,
rejectedSuggestions: 0,
resolvedSuggestions: 0,
staleAnchors: 0,
orphanedAnchors: 0,
},
+6 -8
View File
@@ -413,16 +413,12 @@ export function DocumentDetail() {
[binding, invalidateReview],
);
// "Resolve" = handled outside / no longer applies. The suggestion data model
// only has pending/accepted/rejected today, so we record it as a reject with a
// clear non-disagreement reason for the audit trail. A first-class `resolved`
// status is tracked as a follow-up (child of PAP-10524).
// "Resolve" = handled outside / no longer applies. A first-class `resolved`
// status (distinct from reject/disagreement) keeps the audit trail honest.
const resolveSuggestion = useCallback(
async (suggestion: DocumentSuggestionWithComments) => {
if (!binding) return;
await documentReviewsApi.rejectSuggestion(binding.issueId, binding.key, suggestion.id, {
reason: "Resolved — handled outside review / no longer applies.",
});
await documentReviewsApi.resolveSuggestion(binding.issueId, binding.key, suggestion.id);
invalidateReview();
},
[binding, invalidateReview],
@@ -666,7 +662,9 @@ export function DocumentDetail() {
type="button"
data-testid="document-rail-fab"
onClick={() => setRailOpen(true)}
className="fixed bottom-4 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-full border border-border bg-popover px-4 py-2 text-sm shadow-lg"
// Lift the FAB above the iOS home indicator (§5.8) so it clears the
// safe-area inset and never occludes the last paragraph.
className="fixed bottom-[max(env(safe-area-inset-bottom),1rem)] left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-full border border-border bg-popover px-4 py-2 text-sm shadow-lg"
>
<MessageSquare className="h-4 w-4" aria-hidden="true" />
{(reviewIndex?.counts.openAnchoredThreads ?? 0) + (reviewIndex?.counts.openReviewThreads ?? 0)}
@@ -66,6 +66,9 @@ function suggestion(overrides: Partial<DocumentSuggestionWithComments>): Documen
rejectedByAgentId: null,
rejectedByUserId: null,
rejectedAt: null,
resolvedByAgentId: null,
resolvedByUserId: null,
resolvedAt: null,
createdAt: new Date(now - 2 * HOUR),
updatedAt: new Date(now - 2 * HOUR),
comments: [
@@ -103,6 +106,7 @@ const reviewIndex: DocumentReviewIndex = {
resolvedReviewThreads: 0,
acceptedSuggestions: 0,
rejectedSuggestions: 1,
resolvedSuggestions: 0,
staleAnchors: 1,
orphanedAnchors: 1,
},