bcd194ee5d
The AI agent (MCP + in-app chat) saw ALL comments incl. resolved via two channels, cluttering its context and breaking fragment search. Default now: the agent sees only ACTIVE discussions; resolved is opt-in. Active anchors and threads are always kept. Channel 1 — resolved comment anchors on agent reads (converter option): `convertProseMirrorToMarkdown(content, options?)` gains `options.dropResolvedCommentAnchors` (default false — zero change for every existing caller incl. git-sync). Both `case "comment"` emitters (top-level and the raw-HTML inlineToHtml path) emit BARE text (no `<span data-comment-id>`) when `resolved && the flag`; active anchors keep their wrapper. mcp `getPage` passes the flag; `export_page_markdown` does NOT (lossless export must preserve resolved anchors — that is why it is an opt-in option, not unconditional); `get_page_json` is untouched (lossless PM JSON). Built on the #293 package converter. Channel 2 — `list_comments` default active-only: `listComments(pageId, includeResolved=false)` now returns `{ items, resolvedThreadsHidden }` (was a bare array). By default a RESOLVED top-level thread is hidden wholesale — the root AND every reply anchored to it (a thread is gated only by its root's resolvedAt; a resolved reply under an ACTIVE root stays). `resolvedThreadsHidden` counts hidden threads so the agent knows to re-query. `includeResolved:true` returns everything. The `includeResolved` param is added to both tool registrations (MCP index.ts + in-app ai-chat-tools.service.ts); `DocmostClientLike` signature updated. Server `findPageComments` is NOT touched — the web UI's tabs depend on the full feed; filtering is only at the mcp-client level. All internal call sites (export_page_markdown / checkNewComments / transformPage) updated to `.items` with `includeResolved:true` to keep their full-feed behavior. The comment model is assumed FLAT (a reply's parentCommentId points at the thread root) — documented in the filter; a future reply-of-reply model would need a root-walk there. Tests: resolved-comment-anchors.test.ts (6 — anchor dropped with flag / kept without, for BOTH emitters; active always kept); list-comments-resolved.test.mjs (4 — resolved thread+reply hidden + counter; includeResolved:true returns all; an ACTIVE thread with a RESOLVED reply is NOT hidden). package vitest: 664 passed; tsc clean. mcp: node --test 458 passed; tsc clean. apps/server + git-sync: tsc clean (converter option default-off). NOTE: based on feat/293-B (#293/#326 STEP 5) — the converter lives in the package; this PR is stacked on #333 and its base retargets to develop once #333 merges. mcp/build is gitignored (not committed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
// Import the converter DIRECTLY from src (NOT the docmost-client barrel, which
|
|
// pulls in collaboration.ts and mutates the global DOM at import time), matching
|
|
// the other converter unit tests (see markdown-converter-html-marks.test.ts).
|
|
import { convertProseMirrorToMarkdown } from '../src/lib/markdown-converter.js';
|
|
|
|
// gitmost #328 Channel 1: the `dropResolvedCommentAnchors` converter option
|
|
// hides RESOLVED comment anchors from agent reads while keeping ACTIVE anchors.
|
|
// The option defaults to false (zero behavior change for the lossless git-sync
|
|
// export path). Two emitters read it: the top-level marks loop and the raw-HTML
|
|
// inlineToHtml path (inside columns / spanned table cells).
|
|
|
|
const text = (t: string, marks?: any[]) =>
|
|
marks ? { type: 'text', text: t, marks } : { type: 'text', text: t };
|
|
const para = (...inline: any[]) => ({ type: 'paragraph', content: inline });
|
|
const doc = (...nodes: any[]) => ({ type: 'doc', content: nodes });
|
|
|
|
const commentMark = (commentId: string, resolved: boolean) => ({
|
|
type: 'comment',
|
|
attrs: { commentId, resolved },
|
|
});
|
|
|
|
// A columns node (raw-HTML container) so its children render via the
|
|
// blockToHtml -> inlineToHtml path (the SECOND `case "comment"` emitter).
|
|
const oneColumn = (...blocks: any[]) => ({
|
|
type: 'columns',
|
|
attrs: { layout: 'two' },
|
|
content: [{ type: 'column', content: blocks }],
|
|
});
|
|
|
|
describe('#328 Channel 1 — top-level emitter: dropResolvedCommentAnchors', () => {
|
|
const resolvedDoc = doc(
|
|
para(text('kept '), text('resolved', [commentMark('r1', true)])),
|
|
);
|
|
const activeDoc = doc(
|
|
para(text('kept '), text('active', [commentMark('a1', false)])),
|
|
);
|
|
|
|
it('drops a RESOLVED anchor (bare text) WITH the flag', () => {
|
|
const out = convertProseMirrorToMarkdown(resolvedDoc, {
|
|
dropResolvedCommentAnchors: true,
|
|
});
|
|
expect(out).toBe('kept resolved');
|
|
expect(out).not.toContain('data-comment-id');
|
|
});
|
|
|
|
it('PRESERVES a RESOLVED anchor WITHOUT the flag (default off)', () => {
|
|
const out = convertProseMirrorToMarkdown(resolvedDoc);
|
|
expect(out).toContain(
|
|
'<span data-comment-id="r1" data-resolved="true">resolved</span>',
|
|
);
|
|
});
|
|
|
|
it('KEEPS an ACTIVE anchor in BOTH cases', () => {
|
|
const withFlag = convertProseMirrorToMarkdown(activeDoc, {
|
|
dropResolvedCommentAnchors: true,
|
|
});
|
|
const withoutFlag = convertProseMirrorToMarkdown(activeDoc);
|
|
expect(withFlag).toContain('<span data-comment-id="a1">active</span>');
|
|
expect(withoutFlag).toContain('<span data-comment-id="a1">active</span>');
|
|
});
|
|
});
|
|
|
|
describe('#328 Channel 1 — raw-HTML inlineToHtml emitter (columns)', () => {
|
|
const resolvedCol = doc(
|
|
oneColumn(para(text('resolved', [commentMark('r1', true)]))),
|
|
);
|
|
const activeCol = doc(
|
|
oneColumn(para(text('active', [commentMark('a1', false)]))),
|
|
);
|
|
|
|
it('drops a RESOLVED anchor (bare text) WITH the flag', () => {
|
|
const out = convertProseMirrorToMarkdown(resolvedCol, {
|
|
dropResolvedCommentAnchors: true,
|
|
});
|
|
expect(out).toContain('<p>resolved</p>');
|
|
expect(out).not.toContain('data-comment-id');
|
|
});
|
|
|
|
it('PRESERVES a RESOLVED anchor WITHOUT the flag', () => {
|
|
const out = convertProseMirrorToMarkdown(resolvedCol);
|
|
expect(out).toContain(
|
|
'<span data-comment-id="r1" data-resolved="true">resolved</span>',
|
|
);
|
|
});
|
|
|
|
it('KEEPS an ACTIVE anchor in BOTH cases', () => {
|
|
const withFlag = convertProseMirrorToMarkdown(activeCol, {
|
|
dropResolvedCommentAnchors: true,
|
|
});
|
|
const withoutFlag = convertProseMirrorToMarkdown(activeCol);
|
|
expect(withFlag).toContain('<span data-comment-id="a1">active</span>');
|
|
expect(withoutFlag).toContain('<span data-comment-id="a1">active</span>');
|
|
});
|
|
});
|