[codex] Add document annotations and comments (#6733)

## Thinking Path

> - Paperclip orchestrates AI-agent companies through issues, documents,
runs, and durable company-scoped state.
> - Issue documents are where agents and operators capture plans,
handoffs, and work products.
> - Before this change, document collaboration could only happen through
whole-document edits and detached issue comments.
> - Inline document annotations need stable anchors, revision-aware
persistence, and UI affordances that do not break existing document
editing.
> - This pull request adds company-scoped document annotation threads,
comments, anchor snapshots, API routes, and board UI.
> - The benefit is that operators and agents can discuss specific
document passages without losing context as documents evolve.

## What Changed

- Added document annotation tables, schema exports, shared types,
validators, anchor hashing, and text-anchor helpers.
- Added server-side document annotation services and issue routes for
listing, creating, commenting, resolving, and reopening annotation
threads.
- Included annotation summaries in relevant issue document reads and
backup/recovery document workspace behavior.
- Added React UI for inline document highlights, comment panels, mobile
sheet behavior, deep-link focus, and resolved/open filtering.
- Added annotation design artifacts, Storybook coverage, screenshots,
and a screenshot helper script.
- Rebased the branch onto current `paperclipai/paperclip` `master` and
renumbered the annotation migration from `0085_old_swarm` to
`0091_old_swarm`; the SQL uses `IF NOT EXISTS` guards so environments
that previously applied the old migration number can safely apply the
new one.
- Adjusted the new annotation UI tests to use a local async flush helper
because this workspace's React 19.2.4 export does not expose
`React.act`.

## Verification

- `pnpm run preflight:workspace-links && pnpm exec vitest run
packages/shared/src/document-anchors.test.ts
server/src/__tests__/document-annotation-routes.test.ts
server/src/__tests__/document-annotations-service.test.ts
ui/src/components/DocumentAnnotationLayer.test.tsx
ui/src/components/IssueDocumentAnnotations.test.tsx
ui/src/lib/document-annotation-hash.test.ts
ui/src/lib/document-annotation-selection.test.ts`
- Confirmed `git diff --check` passes.
- Confirmed no `pnpm-lock.yaml` or `.github/workflows/*` files are
included in the PR diff.

## Risks

- Medium risk: this adds new persisted annotation tables and routes
across db/shared/server/ui.
- Migration risk is reduced by moving the branch migration to
`0091_old_swarm` after upstream `0090_resource_memberships` and keeping
the SQL idempotent for old `0085_old_swarm` adopters.
- UI risk is mostly around text range anchoring and panel positioning
across long documents, folded content, and mobile layouts; the PR
includes focused unit coverage and design screenshots.

> 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 coding agent, tool-using software engineering
mode. Context window size is not exposed in this Paperclip 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-05-26 08:41:23 -05:00 committed by GitHub
parent f0ddd24d61
commit b7545823be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 25070 additions and 31 deletions

View file

@ -0,0 +1,288 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const issueId = "11111111-1111-4111-8111-111111111111";
const companyId = "22222222-2222-4222-8222-222222222222";
const otherCompanyId = "33333333-3333-4333-8333-333333333333";
const mockIssueService = vi.hoisted(() => ({
getById: vi.fn(),
assertCheckoutOwner: vi.fn(),
}));
const mockDocumentService = vi.hoisted(() => ({
getIssueDocumentByKey: vi.fn(),
}));
const mockAnnotationService = vi.hoisted(() => ({
listThreadsForIssueDocument: vi.fn(),
getThreadForIssueDocument: vi.fn(),
createThread: vi.fn(),
addComment: vi.fn(),
updateThread: vi.fn(),
remapOpenThreadsForDocument: vi.fn(),
}));
const mockIssueReferenceService = vi.hoisted(() => ({
diffIssueReferenceSummary: vi.fn(() => ({
addedReferencedIssues: [],
removedReferencedIssues: [],
currentReferencedIssues: [],
})),
emptySummary: vi.fn(() => ({ outbound: [], inbound: [] })),
listIssueReferenceSummary: vi.fn(async () => ({ outbound: [], inbound: [] })),
syncAnnotationComment: vi.fn(async () => undefined),
syncComment: vi.fn(async () => undefined),
syncDocument: vi.fn(async () => undefined),
syncIssue: vi.fn(async () => undefined),
}));
const mockHeartbeatService = vi.hoisted(() => ({
wakeup: vi.fn(async () => undefined),
reportRunActivity: vi.fn(async () => undefined),
}));
const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined));
const documentPayload = {
id: "document-1",
companyId,
issueId,
key: "plan",
title: "Plan",
format: "markdown",
body: "Alpha selected text omega",
latestRevisionId: "44444444-4444-4444-8444-444444444444",
latestRevisionNumber: 1,
createdByAgentId: null,
createdByUserId: "board-user",
updatedByAgentId: null,
updatedByUserId: "board-user",
createdAt: new Date("2026-05-14T12:00:00.000Z"),
updatedAt: new Date("2026-05-14T12:00:00.000Z"),
};
const annotationThread = {
id: "55555555-5555-4555-8555-555555555555",
companyId,
issueId,
documentId: "document-1",
documentKey: "plan",
status: "open",
anchorState: "active",
anchorConfidence: "exact",
originalRevisionId: documentPayload.latestRevisionId,
originalRevisionNumber: 1,
currentRevisionId: documentPayload.latestRevisionId,
currentRevisionNumber: 1,
selectedText: "selected text",
prefixText: "Alpha ",
suffixText: " omega",
normalizedStart: 6,
normalizedEnd: 19,
markdownStart: 6,
markdownEnd: 19,
anchorSelector: {
quote: { exact: "selected text", prefix: "Alpha ", suffix: " omega" },
position: { normalizedStart: 6, normalizedEnd: 19, markdownStart: 6, markdownEnd: 19 },
},
createdByAgentId: null,
createdByUserId: "board-user",
resolvedByAgentId: null,
resolvedByUserId: null,
resolvedAt: null,
createdAt: new Date("2026-05-14T12:01:00.000Z"),
updatedAt: new Date("2026-05-14T12:01:00.000Z"),
};
const annotationComment = {
id: "66666666-6666-4666-8666-666666666666",
companyId,
threadId: annotationThread.id,
issueId,
documentId: "document-1",
body: "Please review PAP-1",
authorType: "user",
authorAgentId: null,
authorUserId: "board-user",
createdByRunId: null,
createdAt: new Date("2026-05-14T12:01:00.000Z"),
updatedAt: new Date("2026-05-14T12:01:00.000Z"),
};
function registerModuleMocks() {
vi.doMock("../services/index.js", () => ({
accessService: () => ({ canUser: vi.fn(), hasPermission: vi.fn(async () => false) }),
agentService: () => ({ getById: vi.fn(), list: vi.fn(async () => []) }),
companyService: () => ({ getById: vi.fn(async () => ({ id: companyId, attachmentMaxBytes: 10_000_000 })) }),
documentAnnotationService: () => mockAnnotationService,
documentService: () => mockDocumentService,
environmentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => ({}),
goalService: () => ({}),
heartbeatService: () => mockHeartbeatService,
instanceSettingsService: () => ({
get: vi.fn(async () => ({ id: "settings", general: {} })),
getExperimental: vi.fn(async () => ({})),
getGeneral: vi.fn(async () => ({})),
listCompanyIds: vi.fn(async () => [companyId]),
}),
issueApprovalService: () => ({}),
issueRecoveryActionService: () => ({
getActiveForIssue: vi.fn(async () => null),
listActiveForIssues: vi.fn(async () => new Map()),
}),
issueReferenceService: () => mockIssueReferenceService,
issueService: () => mockIssueService,
issueThreadInteractionService: () => ({
expireRequestConfirmationsSupersededByComment: vi.fn(async () => []),
expireStaleRequestConfirmationsForIssueDocument: vi.fn(async () => []),
}),
logActivity: mockLogActivity,
projectService: () => ({}),
routineService: () => ({ syncRunStatusForIssue: vi.fn(async () => undefined) }),
workProductService: () => ({}),
}));
}
async function createApp(actor: "board" | "agent" = "board", actorCompanyId = companyId) {
const [{ issueRoutes }, { errorHandler }] = await Promise.all([
vi.importActual<typeof import("../routes/issues.js")>("../routes/issues.js"),
vi.importActual<typeof import("../middleware/index.js")>("../middleware/index.js"),
]);
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
(req as any).actor = actor === "agent"
? {
type: "agent",
agentId: "77777777-7777-4777-8777-777777777777",
companyId: actorCompanyId,
runId: "88888888-8888-4888-8888-888888888888",
}
: {
type: "board",
userId: "board-user",
companyIds: [actorCompanyId],
source: "local_implicit",
isInstanceAdmin: false,
};
next();
});
app.use("/api", issueRoutes({} as any, {} as any));
app.use(errorHandler);
return app;
}
describe("document annotation routes", () => {
beforeEach(() => {
vi.resetModules();
vi.doUnmock("../routes/issues.js");
vi.doUnmock("../middleware/index.js");
registerModuleMocks();
vi.clearAllMocks();
mockIssueService.getById.mockResolvedValue({
id: issueId,
companyId,
title: "Annotation API",
status: "in_progress",
assigneeAgentId: null,
});
mockIssueService.assertCheckoutOwner.mockResolvedValue({});
mockDocumentService.getIssueDocumentByKey.mockResolvedValue(documentPayload);
mockAnnotationService.listThreadsForIssueDocument.mockImplementation(async (
_issueId: string,
_key: string,
options?: { includeComments?: boolean },
) => (
options?.includeComments
? [{ ...annotationThread, comments: [annotationComment] }]
: [annotationThread]
));
mockAnnotationService.getThreadForIssueDocument.mockResolvedValue({ ...annotationThread, comments: [annotationComment] });
mockAnnotationService.createThread.mockResolvedValue({ ...annotationThread, comments: [annotationComment] });
mockAnnotationService.addComment.mockResolvedValue(annotationComment);
mockAnnotationService.updateThread.mockResolvedValue({ ...annotationThread, status: "resolved" });
mockAnnotationService.remapOpenThreadsForDocument.mockResolvedValue([]);
});
it("includes compact open annotations without comment bodies by default for agent document reads", async () => {
const res = await request(await createApp("agent"))
.get(`/api/issues/${issueId}/documents/plan`)
.expect(200);
expect(res.body.annotations).toHaveLength(1);
expect(res.body.annotations[0].comments).toBeUndefined();
expect(mockAnnotationService.listThreadsForIssueDocument).toHaveBeenCalledWith(issueId, "plan", {
status: "open",
includeComments: false,
});
});
it("includes annotation comment bodies on document reads only when explicitly requested", async () => {
const res = await request(await createApp("agent"))
.get(`/api/issues/${issueId}/documents/plan?includeAnnotationComments=true`)
.expect(200);
expect(res.body.annotations[0].comments[0].body).toBe("Please review PAP-1");
expect(mockAnnotationService.listThreadsForIssueDocument).toHaveBeenCalledWith(issueId, "plan", {
status: "open",
includeComments: true,
});
});
it("creates annotation threads, syncs references, logs activity, and wakes the assignee", async () => {
mockIssueService.getById.mockResolvedValue({
id: issueId,
companyId,
title: "Annotation API",
status: "todo",
assigneeAgentId: "99999999-9999-4999-8999-999999999999",
});
const res = await request(await createApp())
.post(`/api/issues/${issueId}/documents/plan/annotations`)
.send({
baseRevisionId: documentPayload.latestRevisionId,
baseRevisionNumber: 1,
selector: annotationThread.anchorSelector,
body: "Please review PAP-1",
})
.expect(201);
expect(res.body.id).toBe(annotationThread.id);
expect(mockIssueReferenceService.syncAnnotationComment).toHaveBeenCalledWith(annotationComment.id);
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
action: "issue.document_annotation_thread_created",
}));
expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(
"99999999-9999-4999-8999-999999999999",
expect.objectContaining({
payload: expect.objectContaining({
annotationThreadId: annotationThread.id,
annotationCommentId: annotationComment.id,
}),
}),
);
});
it("rejects agent cross-company annotation reads", async () => {
await request(await createApp("agent", otherCompanyId))
.get(`/api/issues/${issueId}/documents/plan/annotations`)
.expect(403);
});
it("adds annotation comments and resolves threads", async () => {
await request(await createApp())
.post(`/api/issues/${issueId}/documents/plan/annotations/${annotationThread.id}/comments`)
.send({ body: "Reply with PAP-2" })
.expect(201);
expect(mockIssueReferenceService.syncAnnotationComment).toHaveBeenCalledWith(annotationComment.id);
const resolved = await request(await createApp())
.patch(`/api/issues/${issueId}/documents/plan/annotations/${annotationThread.id}`)
.send({ status: "resolved" })
.expect(200);
expect(resolved.body.status).toBe("resolved");
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
action: "issue.document_annotation_thread_resolved",
}));
});
});

View file

@ -0,0 +1,183 @@
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
companies,
createDb,
documentAnnotationAnchorSnapshots,
documentAnnotationComments,
documentAnnotationThreads,
documentRevisions,
documents,
issueDocuments,
issues,
} from "@paperclipai/db";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { documentAnnotationService } from "../services/document-annotations.js";
import { documentService } from "../services/documents.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres document annotation service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
function deferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((promiseResolve, promiseReject) => {
resolve = promiseResolve;
reject = promiseReject;
});
return { promise, resolve, reject };
}
describeEmbeddedPostgres("documentAnnotationService", () => {
let db!: ReturnType<typeof createDb>;
let annotations!: ReturnType<typeof documentAnnotationService>;
let docs!: ReturnType<typeof documentService>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-document-annotations-");
db = createDb(tempDb.connectionString);
annotations = documentAnnotationService(db);
docs = documentService(db);
}, 20_000);
afterEach(async () => {
await db.delete(documentAnnotationAnchorSnapshots);
await db.delete(documentAnnotationComments);
await db.delete(documentAnnotationThreads);
await db.delete(documentRevisions);
await db.delete(issueDocuments);
await db.delete(documents);
await db.delete(issues);
await db.delete(companies);
});
afterAll(async () => {
await tempDb?.cleanup();
});
async function createIssueWithDocument() {
const companyId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(issues).values({
id: issueId,
companyId,
identifier: "PAP-9442",
title: "Annotation race",
description: "Validate annotation revision guards",
status: "in_progress",
priority: "high",
});
const created = await docs.upsertIssueDocument({
issueId,
key: "plan",
title: "Plan",
format: "markdown",
body: "Alpha selected text omega",
});
return { companyId, issueId, document: created.document };
}
it("fails closed when a concurrent document update wins before annotation thread creation commits", async () => {
const { companyId, issueId, document } = await createIssueWithDocument();
const concurrentUpdateCanCommit = deferred<void>();
const concurrentUpdateHasWritten = deferred<void>();
const concurrentUpdate = db.transaction(async (tx) => {
const now = new Date();
const [revision] = await tx
.insert(documentRevisions)
.values({
companyId,
documentId: document.id,
revisionNumber: document.latestRevisionNumber + 1,
title: "Plan",
format: "markdown",
body: "Alpha changed text omega",
changeSummary: "Concurrent edit",
createdAt: now,
})
.returning();
await tx
.update(documents)
.set({
latestBody: "Alpha changed text omega",
latestRevisionId: revision.id,
latestRevisionNumber: document.latestRevisionNumber + 1,
updatedAt: now,
})
.where(eq(documents.id, document.id));
concurrentUpdateHasWritten.resolve();
await concurrentUpdateCanCommit.promise;
});
await concurrentUpdateHasWritten.promise;
let annotationSettled = false;
const annotationResult = annotations
.createThread(
issueId,
"plan",
{
baseRevisionId: document.latestRevisionId!,
baseRevisionNumber: document.latestRevisionNumber,
selector: {
quote: { exact: "selected text", prefix: "Alpha ", suffix: " omega" },
position: { normalizedStart: 6, normalizedEnd: 19, markdownStart: 6, markdownEnd: 19 },
},
body: "Please review this text",
},
{ actorType: "user", actorId: "board-user", userId: "board-user" },
)
.then(
() => ({ status: "fulfilled" as const }),
(error: unknown) => ({ status: "rejected" as const, error }),
)
.finally(() => {
annotationSettled = true;
});
await new Promise((resolve) => setTimeout(resolve, 50));
expect(annotationSettled).toBe(false);
concurrentUpdateCanCommit.resolve();
await concurrentUpdate;
const result = await annotationResult;
expect(result.status).toBe("rejected");
if (result.status === "rejected") {
expect(result.error).toMatchObject({
status: 409,
message: "Annotation anchor requires the current document revision",
details: {
currentRevisionNumber: 2,
},
});
}
const threads = await db.select().from(documentAnnotationThreads);
expect(threads).toHaveLength(0);
});
});

View file

@ -90,6 +90,7 @@ vi.mock("../services/index.js", () => ({
expireStaleRequestConfirmationsForIssueDocument: vi.fn(async () => []),
}),
documentService: () => ({}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
routineService: () => ({}),
workProductService: () => ({}),
}));

View file

@ -82,6 +82,7 @@ function registerModuleMocks() {
agentService: () => ({
getById: vi.fn(async () => null),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => mockFeedbackService,

View file

@ -97,6 +97,7 @@ function registerRouteMocks() {
}));
vi.doMock("../services/documents.js", () => ({
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => mockDocumentService,
}));
@ -116,6 +117,7 @@ function registerRouteMocks() {
accessService: () => mockAccessService,
agentService: () => mockAgentService,
companyService: () => mockCompanyService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => mockDocumentService,
executionWorkspaceService: () => ({}),
feedbackService: () => ({

View file

@ -36,6 +36,7 @@ vi.mock("../services/index.js", () => ({
companyService: () => ({
getById: vi.fn(async () => ({ id: "company-1", attachmentMaxBytes: 10 * 1024 * 1024 })),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({
getIssueDocumentPayload: vi.fn(async () => ({})),
}),

View file

@ -43,6 +43,7 @@ function registerRouteMocks() {
getById: vi.fn(),
}),
companyService: () => mockCompanyService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => ({

View file

@ -81,6 +81,7 @@ function registerServiceMocks() {
agentService: () => ({
getById: vi.fn(async () => null),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => mockExecutionWorkspaceService,
feedbackService: () => ({

View file

@ -79,6 +79,7 @@ function registerModuleMocks() {
}),
accessService: () => mockAccessService,
agentService: () => ({ getById: vi.fn(async () => null) }),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => mockFeedbackService,

View file

@ -123,6 +123,7 @@ vi.mock("../services/index.js", () => ({
}),
accessService: () => mockAccessService,
agentService: () => mockAgentService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => mockFeedbackService,

View file

@ -27,6 +27,7 @@ vi.mock("../services/index.js", () => ({
agentService: () => ({
getById: vi.fn(),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({
getIssueDocumentPayload: vi.fn(async () => ({})),
}),

View file

@ -88,6 +88,7 @@ function registerModuleMocks() {
}));
vi.doMock("../services/documents.js", () => ({
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => mockDocumentsService,
}));
@ -113,6 +114,7 @@ function registerModuleMocks() {
}),
accessService: () => mockAccessService,
agentService: () => mockAgentService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => mockDocumentsService,
executionWorkspaceService: () => ({}),
feedbackService: () => ({}),

View file

@ -48,6 +48,7 @@ function registerModuleMocks() {
agentService: () => ({
getById: vi.fn(async () => null),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => ({

View file

@ -87,6 +87,7 @@ function registerModuleMocks() {
}),
accessService: () => mockAccessService,
agentService: () => mockAgentService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => mockExecutionWorkspaceService,
goalService: () => ({}),

View file

@ -35,6 +35,7 @@ function registerModuleMocks() {
hasPermission: vi.fn(),
}),
agentService: () => mockAgentService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => ({}),

View file

@ -61,6 +61,7 @@ function registerModuleMocks() {
clampIssueListLimit: (value: number) => value,
ISSUE_LIST_DEFAULT_LIMIT: 500,
ISSUE_LIST_MAX_LIMIT: 1000,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => ({

View file

@ -48,6 +48,7 @@ vi.mock("../services/index.js", () => ({
agent: { id: raw },
})),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => ({
@ -116,6 +117,7 @@ function registerModuleMocks() {
agent: { id: raw },
})),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => ({}),
feedbackService: () => ({

View file

@ -95,6 +95,7 @@ function registerRouteMocks() {
}),
accessService: () => mockAccessService,
agentService: () => mockAgentService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => mockExecutionWorkspaceService,
feedbackService: () => mockFeedbackService,

View file

@ -103,6 +103,7 @@ vi.mock("../services/index.js", () => ({
}),
accessService: () => mockAccessService,
agentService: () => mockAgentService,
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => mockDocumentsService,
environmentService: () => mockEnvironmentService,
executionWorkspaceService: () => mockExecutionWorkspaceService,