diff --git a/apps/mcp/src/generated/catalog.ts b/apps/mcp/src/generated/catalog.ts index 1266b99ac2..4cd0a69ba6 100644 --- a/apps/mcp/src/generated/catalog.ts +++ b/apps/mcp/src/generated/catalog.ts @@ -2286,7 +2286,7 @@ export const MCP_TOOL_CATALOG = { { toolName: 'superdoc_comment', description: - 'Manage document comment threads: create, read, update, and delete. To create a comment, first use superdoc_search to find the target text, then pass action "create" with the comment text and a target: {kind:"text", blockId:"", range:{start:, end:}} using the blockId and highlightRange from the search result. For threaded replies, pass "parentId" with the parent comment ID. Action "list" returns all comments with optional pagination (limit, offset) and filtering (includeResolved:true to include resolved). Action "get" retrieves a single comment by ID. Action "update" changes status to "resolved" or marks as internal. Action "delete" removes a comment or reply by ID. Do NOT pass "ref", "id", or "parentId" when creating a new top-level comment; only "action", "text", and "target" are needed.\n\nEXAMPLES:\n 1. {"action":"create","text":"Please review this section.","target":{"kind":"text","blockId":"","range":{"start":5,"end":25}}}\n 2. {"action":"list","limit":20,"offset":0}\n 3. {"action":"update","id":"","status":"resolved"}\n 4. {"action":"delete","id":""}', + 'Manage document comment threads: create, read, update, and delete. To create a comment, first use superdoc_search to find the target text, then pass action "create" with the comment text and a target built from items[0].blocks. For a single-block match use {kind:"text", blockId: items[0].blocks[0].blockId, range: items[0].blocks[0].range}. For a cross-block match use {kind:"text", segments: items[0].blocks.map(b => ({blockId: b.blockId, range: b.range}))}. Do NOT use items[0].highlightRange (snippet-relative, not block-relative) or items[0].target (a SelectionTarget, not accepted by comments.create). For threaded replies, pass "parentId" with the parent comment ID. Action "list" returns all comments with optional pagination (limit, offset) and filtering (includeResolved:true to include resolved). Action "get" retrieves a single comment by ID. Action "update" changes status to "resolved" or marks as internal. Action "delete" removes a comment or reply by ID. Do NOT pass "ref", "id", or "parentId" when creating a new top-level comment; only "action", "text", and "target" are needed.\n\nEXAMPLES:\n 1. {"action":"create","text":"Please review this section.","target":{"kind":"text","blockId":"","range":{"start":5,"end":25}}}\n 2. {"action":"list","limit":20,"offset":0}\n 3. {"action":"update","id":"","status":"resolved"}\n 4. {"action":"delete","id":""}', inputSchema: { type: 'object', properties: { diff --git a/apps/mcp/src/generated/mcp-prompt.ts b/apps/mcp/src/generated/mcp-prompt.ts index 25cb3b0fdf..46435c5288 100644 --- a/apps/mcp/src/generated/mcp-prompt.ts +++ b/apps/mcp/src/generated/mcp-prompt.ts @@ -416,7 +416,7 @@ superdoc_search({select: {type: "text", pattern: "target phrase"}, require: "fir superdoc_comment({ action: "create", text: "Please review this section.", - target: {kind: "text", blockId: "", range: {start: , end: }} + target: {kind: "text", blockId: "", range: {start: , end: }} }) \`\`\` diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index c47287b509..7920d9b8d0 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -397,7 +397,7 @@ export const INTENT_GROUP_META: Record = { toolName: 'superdoc_comment', description: 'Manage document comment threads: create, read, update, and delete. ' + - 'To create a comment, first use superdoc_search to find the target text, then pass action "create" with the comment text and a target: {kind:"text", blockId:"", range:{start:, end:}} using the blockId and highlightRange from the search result. ' + + 'To create a comment, first use superdoc_search to find the target text, then pass action "create" with the comment text and a target built from items[0].blocks. For a single-block match use {kind:"text", blockId: items[0].blocks[0].blockId, range: items[0].blocks[0].range}. For a cross-block match use {kind:"text", segments: items[0].blocks.map(b => ({blockId: b.blockId, range: b.range}))}. Do NOT use items[0].highlightRange (snippet-relative, not block-relative) or items[0].target (a SelectionTarget, not accepted by comments.create). ' + 'For threaded replies, pass "parentId" with the parent comment ID. ' + 'Action "list" returns all comments with optional pagination (limit, offset) and filtering (includeResolved:true to include resolved). ' + 'Action "get" retrieves a single comment by ID. Action "update" changes status to "resolved" or marks as internal. Action "delete" removes a comment or reply by ID. ' + diff --git a/packages/sdk/langs/browser/src/system-prompt.ts b/packages/sdk/langs/browser/src/system-prompt.ts index c5405691d7..af0d99b5c4 100644 --- a/packages/sdk/langs/browser/src/system-prompt.ts +++ b/packages/sdk/langs/browser/src/system-prompt.ts @@ -371,7 +371,7 @@ superdoc_search({select: {type: "text", pattern: "target phrase"}, require: "fir superdoc_comment({ action: "create", text: "Please review this section.", - target: {kind: "text", blockId: "", range: {start: , end: }} + target: {kind: "text", blockId: "", range: {start: , end: }} }) \`\`\` diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index f58224266c..7cba4ad117 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -365,7 +365,7 @@ superdoc_search({select: {type: "text", pattern: "target phrase"}, require: "fir superdoc_comment({ action: "create", text: "Please review this section.", - target: {kind: "text", blockId: "", range: {start: , end: }} + target: {kind: "text", blockId: "", range: {start: , end: }} }) ``` diff --git a/packages/sdk/tools/system-prompt-mcp.md b/packages/sdk/tools/system-prompt-mcp.md index 4d3dbcfd7b..20f5518d31 100644 --- a/packages/sdk/tools/system-prompt-mcp.md +++ b/packages/sdk/tools/system-prompt-mcp.md @@ -414,7 +414,7 @@ superdoc_search({select: {type: "text", pattern: "target phrase"}, require: "fir superdoc_comment({ action: "create", text: "Please review this section.", - target: {kind: "text", blockId: "", range: {start: , end: }} + target: {kind: "text", blockId: "", range: {start: , end: }} }) ``` diff --git a/packages/sdk/tools/system-prompt.md b/packages/sdk/tools/system-prompt.md index 35c44db92e..5f0fef82a0 100644 --- a/packages/sdk/tools/system-prompt.md +++ b/packages/sdk/tools/system-prompt.md @@ -369,7 +369,7 @@ superdoc_search({select: {type: "text", pattern: "target phrase"}, require: "fir superdoc_comment({ action: "create", text: "Please review this section.", - target: {kind: "text", blockId: "", range: {start: , end: }} + target: {kind: "text", blockId: "", range: {start: , end: }} }) ``` diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/query-match-adapter.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/query-match-adapter.test.ts index ff4d266a0b..2033fe1100 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/query-match-adapter.test.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/query-match-adapter.test.ts @@ -871,3 +871,54 @@ describe('queryMatchAdapter — cardinality', () => { expect(result.total).toBe(2); }); }); + +// --------------------------------------------------------------------------- +// Wire-shape regression guard +// +// Pins the public contract of items[] so callers (LLM tool prompts, agent +// guides) cannot drift back to fields that do not exist on the wire. The +// `superdoc_comment` MCP description previously told agents to use +// `items[0].context.textRanges[0]`, which is a field on `doc.find` output +// (the legacy sibling) and not on `doc.query.match` (where `superdoc_search` +// dispatches). Comments use `TextAddress | TextTarget` built from +// `items[0].blocks[*]`. +// --------------------------------------------------------------------------- + +describe('queryMatchAdapter — wire-shape contract', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockedDeps.getRevision.mockReturnValue('rev-1'); + }); + + it('emits items without a `context` field; range data lives on blocks[i]', () => { + const candidates = [{ nodeId: 'p1', pos: 0, end: 22, text: 'Title Body text here' }]; + const editor = makeEditorWithBlocks(candidates); + setupBlockIndex(candidates.map(({ nodeId, pos, end }) => ({ nodeId, pos, end }))); + setupFindResult({ + matches: [{ kind: 'text', blockId: 'p1' }], + context: [{ textRanges: [{ kind: 'text', blockId: 'p1', range: { start: 5, end: 17 } }] }], + total: 1, + }); + mockedDeps.captureRunsInRange.mockReturnValue(captured([capturedRun(5, 17, [])])); + + const result = queryMatchAdapter(editor, { + select: { type: 'text', pattern: 'Body text he' }, + }); + + const item = result.items[0] as any; + + // The wire output of `query.match` has no `context` and no `textRanges`. + // Adding either would silently re-enable the previously-broken agent + // guidance that pointed at `items[0].context.textRanges[0]`. + expect(item.context).toBeUndefined(); + expect(item.textRanges).toBeUndefined(); + + // The block-relative range a comment target needs lives on blocks[i].range. + expect(item.blocks[0].blockId).toBe('p1'); + expect(item.blocks[0].range).toEqual({ start: 5, end: 17 }); + + // `target` is a SelectionTarget (kind:'selection'), which `comments.create` + // does NOT accept (it takes TextAddress | TextTarget, kind:'text'). + expect(item.target.kind).toBe('selection'); + }); +});