From 2f4cbf2a1bd69361753d2a863cbcc91cf21a14a2 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 11:25:06 -0300 Subject: [PATCH 01/26] feat(sdk): update tool definitions for efficient multi-block workflows - superdoc_edit: emphasize markdown insert for multi-section creation - superdoc_create: direct to markdown/mutations for multiple items - superdoc_mutations: document create steps and batch format pattern - superdoc_format: direct to mutations for multi-item formatting - superdoc_search: clarify ref lifecycle within vs across batches - system-prompt: add efficient document creation workflow --- .../src/contract/operation-definitions.ts | 69 +++++++++++++------ .../prompt-templates/system-prompt-core.md | 32 +++++++-- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 239eb066cd..4ceaef39b0 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -105,9 +105,9 @@ export const INTENT_GROUP_META: Record = { search: { toolName: 'superdoc_search', description: - 'Refs expire after any mutation; always re-search before the next edit. ' + 'Find text patterns or nodes in the document and get ref handles for targeting edits and formatting. ' + - 'Use this to locate content before calling superdoc_edit or superdoc_format. ' + + 'Refs expire after any mutation that changes the document. Re-search before the next edit when using individual tools (superdoc_edit, superdoc_format). ' + + 'Within a superdoc_mutations batch, selectors in "where" clauses resolve automatically at compile time; no manual re-searching needed between steps. ' + 'Text search returns handle.ref covering only the matched substring. Node search finds blocks by type (paragraph, heading, table, listItem, etc.). ' + 'The "require" parameter controls match cardinality: "first" returns one match, "all" returns every match, "exactlyOne" fails if not exactly one match. ' + 'Supports scoping via "within" to search inside a single block. ' + @@ -148,13 +148,24 @@ export const INTENT_GROUP_META: Record = { 'Modify document text: insert new content, replace existing text, delete a range, or undo/redo. ' + 'Use this for single text modifications. For 2+ edits that must succeed or fail atomically, use superdoc_mutations instead. ' + 'For replace and delete, pass a "ref" from superdoc_search or superdoc_get_content blocks. A search ref covers only the matched substring; a block ref covers the entire block text, so use block refs when rewriting or shortening whole paragraphs. ' + - 'Insert supports plain text (default), markdown, or html via the "type" parameter. Use "placement" (before, after, insideStart, insideEnd) to control position relative to the target. ' + - 'Supports "dryRun" to preview changes and "changeMode: tracked" to record edits as tracked changes. ' + + 'Insert supports plain text (default), markdown, or html via the "type" parameter. ' + + 'To create a document with multiple headings and paragraphs, use action "insert" with type "markdown" and placement "end". ' + + 'The markdown parser creates proper Heading styles from # markers (# = Heading1, ## = Heading2), bold from **text**, italic from *text*, and numbered/bullet lists. ' + + 'This is the most efficient way to build document structure: one call creates all sections. Follow up with superdoc_mutations format.apply steps to apply formatting (color, font, alignment) that markdown cannot express. ' + + 'Use "placement" (before, after, insideStart, insideEnd) to control position relative to the target. ' + + 'Supports "dryRun" to preview changes and "changeMode: tracked" to record edits as tracked changes (not supported for markdown/html inserts). ' + 'Do NOT build "target" objects manually when a ref is available; prefer "ref" for simpler, more reliable targeting.', inputExamples: [ { action: 'replace', ref: '', text: 'new text here' }, { action: 'insert', value: 'Appended paragraph.', placement: 'insideEnd' }, { action: 'insert', ref: '', value: 'Inserted before.', placement: 'before' }, + { + action: 'insert', + type: 'markdown', + placement: 'end', + value: + '# Section Title\n\nParagraph content here.\n\n# Another Section\n\nMore content with **bold** and *italic*.', + }, { action: 'delete', ref: '' }, { action: 'undo' }, ], @@ -162,12 +173,11 @@ export const INTENT_GROUP_META: Record = { create: { toolName: 'superdoc_create', description: - 'You MUST call superdoc_format after this tool to match document styling. ' + - 'Create a single paragraph, heading, or table in the document. Returns a nodeId for chaining subsequent creates and for use as a block target in superdoc_format. ' + - 'When the user asks for a "heading", use action "heading" with a level (default 1). Use action "paragraph" only when the user asks for regular body text. ' + - 'Before creating, call superdoc_get_content blocks to read formatting from regular body text paragraphs (non-empty, non-title blocks with alignment "justify" or "left"). ' + - 'After creating, re-fetch blocks with superdoc_get_content to get a fresh ref for the new block, then apply TWO format calls: (1) superdoc_format action "inline" for character styling, AND (2) superdoc_format action "set_alignment" with the block target for paragraph alignment. Both calls are REQUIRED. ' + - 'For body paragraphs: inline {bold:false, underline:false, fontFamily, fontSize, color from body blocks}, alignment "justify". Ignore underline:true from blocks data for body text; it is a style artifact. For headings: inline {bold:true, underline:true, fontSize scaled up, fontFamily, color}, alignment "center". ' + + 'Create a single paragraph, heading, or table. Returns nodeId and ref for the created block. ' + + 'For creating multiple headings and paragraphs at once, prefer superdoc_edit with type "markdown" (one call for all structure) instead of calling superdoc_create repeatedly. ' + + 'Use superdoc_create when you need to add one block at a specific position relative to another block. ' + + 'After creating, the returned ref is valid for ONE immediate superdoc_format call. For subsequent operations, re-fetch blocks with superdoc_get_content to get fresh refs (refs expire after any mutation). ' + + 'When the user asks for a "heading", use action "heading" with a level (default 1). Use action "paragraph" for regular body text. ' + 'Position with "at": {kind:"documentEnd"} (default), {kind:"documentStart"}, or {kind:"after"/"before", target:{kind:"block", nodeType, nodeId}} for relative placement. ' + 'When creating multiple items in sequence, use the previous response nodeId as the next "at" target to maintain correct ordering. ' + 'Do NOT use newlines in "text" to create multiple paragraphs; call this tool separately for each one.', @@ -190,7 +200,9 @@ export const INTENT_GROUP_META: Record = { format: { toolName: 'superdoc_format', description: - 'Change text and paragraph formatting. Use this after superdoc_create to style new content, or with a search ref to restyle existing text. ' + + 'Change text and paragraph formatting. ' + + 'To format multiple items at once, use superdoc_mutations with format.apply steps instead of calling this tool repeatedly. Use require "all" with a node selector to format every heading or paragraph in one batch. ' + + 'Use this tool for single-item formatting when you have a valid ref or nodeId. ' + 'Action "inline" applies character formatting (bold, italic, underline, color, fontSize, fontFamily, highlight, strike, vertAlign) to a text range via "ref". ' + 'Action "set_style" applies a named paragraph style by styleId (get available styles from superdoc_get_content info). ' + 'Actions "set_alignment", "set_indentation", "set_spacing", "set_direction", and "set_flow_options" change paragraph-level properties and require a block target: {kind:"block", nodeType:"paragraph", nodeId:""}, NOT a ref. ' + @@ -198,8 +210,7 @@ export const INTENT_GROUP_META: Record = { 'Supports "dryRun" and "changeMode: tracked" for inline formatting. Paragraph-level actions do NOT support tracked changes. ' + 'Do NOT use a search ref for paragraph-level actions; they require a block target with nodeId. ' + 'Do NOT use {kind:"block", start:{kind:"nodeEdge",...}} or selection-like structures for paragraph actions. ONLY {kind:"block", nodeType, nodeId} is accepted. ' + - 'Do NOT issue multiple superdoc_format calls in parallel; each call invalidates refs for subsequent calls. Format one block at a time. ' + - 'Do NOT hardcode formatting values; always read them from superdoc_get_content blocks and replicate.', + 'Do NOT issue multiple superdoc_format calls in parallel; each call invalidates refs for subsequent calls.', inputExamples: [ { action: 'inline', ref: '', inline: { bold: true } }, { @@ -299,13 +310,14 @@ export const INTENT_GROUP_META: Record = { toolName: 'superdoc_mutations', description: 'All steps succeed or all fail; no partial application. ' + - 'Execute multiple text edits atomically in a single batch. Use this INSTEAD OF multiple sequential superdoc_edit calls when you need 2+ text changes that should succeed or fail together. ' + - 'Each step has an id (e.g. "s1"), an op (text.rewrite, text.insert, text.delete, format.apply, assert), a "where" clause for targeting ({by:"select", select:{...}, require:"first"|"exactlyOne"|"all"} or {by:"ref", ref:"..."}), and "args" with operation-specific parameters. ' + - 'Action "preview" dry-runs the plan without modifying the document. Action "apply" executes it. ' + - 'CRITICAL: split mutations by phase. Text mutations (text.rewrite, text.insert, text.delete) go in one call. Formatting (format.apply) goes in a separate call with fresh refs from a new superdoc_search. ' + - 'Do NOT create two steps that target overlapping text in the same block; combine them into a single text.rewrite step. Overlapping steps fail with PLAN_CONFLICT_OVERLAP. ' + - 'Do NOT use this for single edits; use superdoc_edit instead. ' + - 'Do NOT mix text mutations and formatting in the same call.', + 'Execute multiple operations atomically in one batch. Use this for any workflow needing 2+ changes. ' + + 'Supported step types: text (text.rewrite, text.insert, text.delete), format (format.apply), create (create.heading, create.paragraph, create.table), assert. ' + + 'Each step has an id, an op, a "where" clause for targeting ({by:"select", select:{...}, require:"first"|"exactlyOne"|"all"|"last"} or {by:"ref", ref:"..."}), and "args" with operation-specific parameters. ' + + 'For create steps, "where" targets an existing anchor block and args.position ("before" or "after") controls placement. Sequential creates targeting the same anchor maintain correct order via internal position mapping. ' + + 'For format.apply with require "all", use a node selector to format every heading or paragraph at once: {by:"select", select:{type:"node", nodeType:"heading"}, require:"all"}. ' + + 'Selectors resolve at compile time (before execution). This means format.apply steps CANNOT target content created by earlier create steps in the same batch. Split creates and formatting into separate batches: first a mutations call with creates, then a mutations call with format.apply. ' + + 'Action "preview" dry-runs the plan. Action "apply" executes it. ' + + 'Do NOT create two steps that target overlapping text in the same block; combine them into a single text.rewrite step.', inputExamples: [ { action: 'apply', @@ -326,6 +338,23 @@ export const INTENT_GROUP_META: Record = { }, ], }, + { + action: 'apply', + steps: [ + { + id: 'f1', + op: 'format.apply', + where: { by: 'select', select: { type: 'node', nodeType: 'heading' }, require: 'all' }, + args: { inline: { color: '#FF0000' } }, + }, + { + id: 'f2', + op: 'format.apply', + where: { by: 'select', select: { type: 'text', pattern: 'Confidential Information' }, require: 'all' }, + args: { inline: { bold: true } }, + }, + ], + }, ], }, }; diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index 355c857fee..0ba82d9c2b 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -18,9 +18,9 @@ Every editing tool needs a **target** telling the API *where* to apply the chang - **From blocks data**: Each block has a `ref` (pass directly to superdoc_edit or superdoc_format) and a `nodeId` (for building `at` positions with superdoc_create). - **From superdoc_search**: Returns `handle.ref` covering the matched text. Use search when you need to find text patterns, not when you already know which block to target. -- **From superdoc_create**: Returns `nodeId` for chaining creates and building block targets. Re-fetch blocks after create to get a fresh ref before formatting. +- **From superdoc_create**: Returns `nodeId` and `ref`. The ref is valid for one immediate format call. For subsequent operations, re-fetch blocks to get fresh refs. -**Refs expire after any mutation.** Always re-search or re-read blocks before the next operation. +**Refs expire after any mutation** between separate tool calls. Within a superdoc_mutations batch, selectors resolve automatically — no manual re-searching between steps. ## Common workflows @@ -128,9 +128,33 @@ Use preset "disc" for bullets, "decimal" for numbered. WARNING: the range conver 3. To change a bullet list to numbered: `superdoc_list({action: "set_type", target: {kind: "block", nodeType: "listItem", nodeId: ""}, kind: "ordered"})` +### Create a multi-section document efficiently + +**Step 1: Create all structure in one call using markdown insert:** + +``` +superdoc_edit({action: "insert", type: "markdown", placement: "end", + value: "# Definitions\n\n\"Confidential Information\" means any non-public information...\n\n# Obligations\n\nThe Receiving Party agrees to maintain confidentiality...\n\n# Governing Law\n\nThis Agreement shall be governed by the laws of..."}) +``` + +This creates headings with proper Heading1 style, paragraphs, bold (**text**), italic (*text*), lists, and tables in one call. Markdown cannot express color, font-size, or alignment — those require step 2. + +**Step 2: Apply formatting in one batch:** + +``` +superdoc_mutations({action: "apply", steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "node", nodeType: "heading"}, require: "all"}, args: {inline: {color: "#FF0000"}}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Confidential Information"}, require: "all"}, args: {inline: {bold: true}}} +]}) +``` + +Use `require: "all"` with a node selector to format every heading at once instead of formatting one at a time. + +Total: 4 calls (open, insert, format batch, save) instead of 40+. + ### Batch multiple text edits atomically -Use superdoc_mutations when you need 2+ text changes that must succeed or fail together: +Use superdoc_mutations for 2+ text changes, format changes, or a combination: ``` superdoc_mutations({ @@ -143,7 +167,7 @@ superdoc_mutations({ }) ``` -Split mutations by phase: text mutations (text.rewrite, text.insert, text.delete) in one call, then formatting (format.apply) in a separate call with fresh refs from a new superdoc_search. +Selectors resolve at compile time (before execution). This means format.apply steps CANNOT target content created by create steps in the same batch — the new content does not exist yet when selectors compile. Split creates and formatting into separate batches. Never create two steps targeting overlapping text in the same block. Combine them into a single text.rewrite instead. From 039a0361e4b8693753ddecbce645cee33a2a863f Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 12:52:23 -0300 Subject: [PATCH 02/26] feat(evals,sdk): add efficient workflow patterns to all agent touchpoints - Update provider SUPERDOC_SYSTEM_PROMPT with markdown insert and mutations batch examples (what CC actually reads as system prompt) - Update Codex AGENTS.md with same efficient patterns - Update MCP header prompt with "when to use which tool" guide - Increase CC maxTurns from 20 to 35 (both CC failures were at 21) - Regenerate SDK artifacts and rebuild MCP server --- evals/providers/claude-code-agent.mjs | 33 +++++++++++++---- evals/providers/codex-agent.mjs | 29 +++++++++++++-- .../system-prompt-mcp-header.md | 36 +++++++++++++++---- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/evals/providers/claude-code-agent.mjs b/evals/providers/claude-code-agent.mjs index 0b59cdb487..adf411b3cf 100644 --- a/evals/providers/claude-code-agent.mjs +++ b/evals/providers/claude-code-agent.mjs @@ -50,13 +50,32 @@ const SUPERDOC_SYSTEM_PROMPT = `You have a SuperDoc MCP server connected with to IMPORTANT: You MUST use the SuperDoc MCP tools for ALL .docx operations. Do NOT use Bash with unzip, python-docx, mammoth, or manual XML parsing. -Workflow: -1. superdoc_open(path) → returns session_id -2. Use superdoc_get_content, superdoc_search, superdoc_edit, superdoc_format, superdoc_create, superdoc_comment, superdoc_track_changes, superdoc_mutations with the session_id -3. superdoc_save(session_id) to persist changes -4. superdoc_close(session_id) when done +## Efficient workflows -These tools handle OOXML format correctly and preserve document structure. Raw XML manipulation will corrupt the document.`; +### Creating multiple headings and paragraphs + +Use superdoc_edit with type "markdown" to create ALL structure in one call: + +superdoc_edit({action: "insert", type: "markdown", placement: "end", value: "# Heading 1\\n\\nParagraph text...\\n\\n# Heading 2\\n\\nMore text..."}) + +This creates proper Heading styles from # markers, bold from **text**, italic from *text*, and lists. One call replaces many superdoc_create calls. + +### Applying formatting to multiple items at once + +Use superdoc_mutations with format.apply and require "all" to batch formatting: + +superdoc_mutations({action: "apply", steps: [{id: "f1", op: "format.apply", where: {by: "select", select: {type: "node", nodeType: "heading"}, require: "all"}, args: {inline: {color: "#FF0000"}}}]}) + +This formats every heading at once instead of formatting one at a time. + +### Standard workflow + +1. superdoc_open(path) → session_id +2. For reading: superdoc_get_content +3. For creating structure: superdoc_edit with type "markdown" (preferred for multiple blocks) or superdoc_create (for a single block at a specific position) +4. For formatting: superdoc_mutations with format.apply steps (preferred for multiple items) or superdoc_format (for a single item) +5. For text edits: superdoc_edit (single) or superdoc_mutations (batch) +6. superdoc_save then superdoc_close`; const SUPERDOC_CLI_AGENTS_MD = `# AGENTS.md @@ -188,7 +207,7 @@ export default class ClaudeCodeBenchmarkProvider { model, allowedTools: this.config.allowedTools || ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep'], disallowedTools: this.config.disallowedTools, - maxTurns: this.config.maxTurns || 20, + maxTurns: this.config.maxTurns || 35, permissionMode: 'bypassPermissions', allowDangerouslySkipPermissions: true, settingSources: [], // SDK isolation mode: don't load user MCP servers (Linear, Excalidraw, etc.) diff --git a/evals/providers/codex-agent.mjs b/evals/providers/codex-agent.mjs index 047ffc8dd7..57b058f4ed 100644 --- a/evals/providers/codex-agent.mjs +++ b/evals/providers/codex-agent.mjs @@ -43,9 +43,34 @@ const SUPERDOC_MCP_AGENTS_MD = `# AGENTS.md You have a SuperDoc MCP server available. Use it for ALL .docx file operations. **Do NOT** use unzip, python-docx, mammoth, sed, or manual XML editing on .docx files. -**Do** use the superdoc_* MCP tools: superdoc_open → superdoc_get_content/search/edit → superdoc_save → superdoc_close. -The SuperDoc tools handle OOXML format correctly and preserve document structure. +## Efficient workflows + +### Creating multiple headings and paragraphs + +Use superdoc_edit with type "markdown" to create ALL structure in one call: + +\`\`\` +superdoc_edit({action: "insert", type: "markdown", placement: "end", value: "# Heading\\n\\nParagraph...\\n\\n# Heading 2\\n\\nMore text..."}) +\`\`\` + +This creates proper Heading styles from # markers, bold from **text**, italic from *text*. One call replaces many superdoc_create calls. + +### Applying formatting to multiple items at once + +Use superdoc_mutations with format.apply and require "all": + +\`\`\` +superdoc_mutations({action: "apply", steps: [{id: "f1", op: "format.apply", where: {by: "select", select: {type: "node", nodeType: "heading"}, require: "all"}, args: {inline: {color: "#FF0000"}}}]}) +\`\`\` + +### Standard workflow + +1. superdoc_open → session_id +2. Create structure: superdoc_edit with type "markdown" (multiple blocks) or superdoc_create (single block) +3. Format: superdoc_mutations format.apply (batch) or superdoc_format (single item) +4. Text edits: superdoc_edit or superdoc_mutations +5. superdoc_save → superdoc_close `; const SUPERDOC_CLI_AGENTS_MD = `# AGENTS.md diff --git a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md index 844b23d627..d980bf2b28 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md @@ -6,14 +6,36 @@ These tools handle the OOXML format correctly and preserve document structure. ## Session lifecycle -Every interaction requires a session. Follow this workflow: +1. `superdoc_open({path: "/path/to/file.docx"})` — returns `session_id`. Opening a non-existent path creates a blank document. +2. Pass `session_id` to every subsequent tool call. +3. Read, edit, format the document using the tools below. +4. `superdoc_save({session_id})` — writes changes to disk. +5. `superdoc_close({session_id})` — releases the session. Always close when done. -1. `superdoc_open({file: "/path/to/file.docx"})` — returns `session_id` -2. Pass `session_id` to every subsequent tool call -3. Use intent tools (superdoc_search, superdoc_edit, etc.) to read and modify content -4. `superdoc_save({session_id})` — writes changes to disk (optional `out` for save-as) -5. `superdoc_close({session_id})` — releases the session +## Efficient patterns (use these instead of calling tools one at a time) -Opening a non-existent path creates a blank document. Always close sessions when done. +**Creating multiple headings and paragraphs — use markdown insert (one call):** +``` +superdoc_edit({action: "insert", type: "markdown", placement: "end", + value: "# Section Title\n\nParagraph content.\n\n# Another Section\n\nMore content with **bold**."}) +``` +This creates proper Heading styles from # markers. One call replaces many superdoc_create calls. + +**Formatting multiple items at once — use mutations batch (one call):** +``` +superdoc_mutations({action: "apply", steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "node", nodeType: "heading"}, require: "all"}, args: {inline: {color: "#FF0000"}}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "important term"}, require: "all"}, args: {inline: {bold: true}}} +]}) +``` +Use require "all" to format every match at once. Selectors resolve before execution, so format targets must exist in the document before the batch runs. + +**When to use which tool:** +- Creating multiple blocks → `superdoc_edit` with type "markdown" +- Creating one block at a specific position → `superdoc_create` +- Formatting multiple items → `superdoc_mutations` with format.apply steps +- Formatting one item → `superdoc_format` +- Multiple text edits → `superdoc_mutations` +- Single text edit → `superdoc_edit` From dc427eb12fe0b12511c38acf2852779de52440a3 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 13:17:15 -0300 Subject: [PATCH 03/26] feat(evals): enable tool search to reduce token overhead --- evals/providers/claude-code-agent.mjs | 1 + evals/providers/codex-agent.mjs | 1 + 2 files changed, 2 insertions(+) diff --git a/evals/providers/claude-code-agent.mjs b/evals/providers/claude-code-agent.mjs index adf411b3cf..c66e68fd1d 100644 --- a/evals/providers/claude-code-agent.mjs +++ b/evals/providers/claude-code-agent.mjs @@ -169,6 +169,7 @@ export default class ClaudeCodeBenchmarkProvider { try { const env = { ...process.env }; + env.ENABLE_TOOL_SEARCH = 'auto:5'; if (!this.config.superdocOnPath) { env.PATH = env.PATH.split(':') .filter(p => !p.includes('superdoc')) diff --git a/evals/providers/codex-agent.mjs b/evals/providers/codex-agent.mjs index 57b058f4ed..915999706e 100644 --- a/evals/providers/codex-agent.mjs +++ b/evals/providers/codex-agent.mjs @@ -171,6 +171,7 @@ export default class CodexBenchmarkProvider { NODE_ENV: 'production', FORCE_COLOR: '0', NO_COLOR: '1', + ENABLE_TOOL_SEARCH: 'auto:5', }; // Install vendor DOCX skill (Anthropic's docx skill) as AGENTS.md From 091da29f2204fcb8b9c0c8b47c6c2ce6bca0ee31 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 13:18:27 -0300 Subject: [PATCH 04/26] docs(ai): add markdown insert pattern and formatting guidance --- apps/docs/ai/agents/best-practices.mdx | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/docs/ai/agents/best-practices.mdx b/apps/docs/ai/agents/best-practices.mdx index c28c54d612..3b46141f62 100644 --- a/apps/docs/ai/agents/best-practices.mdx +++ b/apps/docs/ai/agents/best-practices.mdx @@ -67,6 +67,24 @@ Instruct the LLM to plan all edits before calling tools. A well-structured promp Batch multiple changes only when atomic execution is genuinely helpful — use `superdoc_mutations` for that. +## Prefer markdown insert for multi-block creation + +When you need to create multiple headings and paragraphs in one operation, use `superdoc_edit` with `type: "markdown"` instead of calling `superdoc_create` once per block. A single markdown insert produces the entire structure in one call. + +```json +{ + "tool": "superdoc_edit", + "intent": "insert", + "target": { "type": "end" }, + "content": { + "type": "markdown", + "value": "## Executive Summary\n\nThis agreement governs the terms of service.\n\n## Key Provisions\n\nThe following provisions apply to all parties." + } +} +``` + +After inserting, apply formatting in a single `superdoc_mutations` batch using `format.apply` steps — one step per block or range. This reduces a workflow that might otherwise take 40+ calls down to 4: read, search, insert, format. + ## Use focused tools; `superdoc_mutations` is an escape hatch For straightforward edits, use the focused intent tools (`superdoc_edit`, `superdoc_format`, `superdoc_create`, `superdoc_list`, `superdoc_comment`). They validate arguments, give clear errors, and are easier for models to call correctly. @@ -90,6 +108,27 @@ try { } ``` +## Choose formatting values from the document + +Don't hardcode formatting values. Read them from the document's existing content and match what's already there. + +**Body text:** Read `fontFamily`, `fontSize`, and `color` from non-empty paragraphs with `alignment: "justify"` or `alignment: "left"`. Set `bold: false` for body paragraphs. + +Many DOCX documents report `underline: true` on all blocks due to style inheritance. This is a DOCX artifact — not intentional formatting. Do not carry it forward when inserting new paragraphs. + +**Headings:** Read from existing heading blocks in the document. Scale `fontSize` up relative to body text. Headings are typically bold and sometimes centered — confirm against what's already in the document rather than assuming. + +```typescript +// Get content first, find a representative body paragraph +const content = await superdoc.getContent(); +const bodyParagraph = content.blocks.find( + (b) => b.type === 'paragraph' && b.text?.trim().length > 0 +); +const { fontFamily, fontSize, color } = bodyParagraph?.formatting ?? {}; + +// Use those values when formatting inserted content +``` + ## Add examples for repeatable workflows If the same kind of edit runs across many documents (e.g., always rewriting a specific clause, always adding a comment to a section), include a concrete tool call example in your system prompt. Models that see a working example of the exact tool invocation produce correct calls more reliably than models that only see the schema. From 2c943773332b29201262c3f74b49b8d6d3e1c9f3 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 13:18:37 -0300 Subject: [PATCH 05/26] docs(ai): add efficient patterns to MCP how-to-use guide --- apps/docs/ai/mcp/how-to-use.mdx | 61 +++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/apps/docs/ai/mcp/how-to-use.mdx b/apps/docs/ai/mcp/how-to-use.mdx index 84f37bbca3..26c5149424 100644 --- a/apps/docs/ai/mcp/how-to-use.mdx +++ b/apps/docs/ai/mcp/how-to-use.mdx @@ -19,16 +19,71 @@ superdoc_open → superdoc_get_content / superdoc_search → intent tools → su 4. `superdoc_save` writes changes to disk 5. `superdoc_close` releases the session +## Efficient patterns + +### Create multiple sections at once + +Use `superdoc_edit` with `type: "markdown"` to insert structured content in a single call. It parses markdown into proper document nodes — headings, paragraphs, bold, italic, and lists. + +``` +superdoc_edit({ + session_id, + action: "insert", + type: "markdown", + placement: "end", + content: "# Background\n\nThis section covers the project history.\n\n# Next steps\n\n1. Review the proposal\n2. Assign owners\n3. Set a deadline" +}) +``` + +Markdown syntax maps to document styles: + +| Markdown | Document node | +|----------|--------------| +| `# Heading` | Heading 1 | +| `## Heading` | Heading 2 | +| `**text**` | Bold | +| `*text*` | Italic | +| `- item` | Bullet list | +| `1. item` | Numbered list | + +### Format multiple items at once + +Use `superdoc_mutations` with `format.apply` steps to apply formatting across the document in one atomic call. + +``` +superdoc_mutations({session_id, action: "apply", atomic: true, changeMode: "direct", steps: [ + {id: "f1", op: "format.apply", where: {by: "node", select: {type: "node", nodeType: "heading", level: 1}}, args: {inline: {bold: true}}, require: "all"}, + {id: "f2", op: "format.apply", where: {by: "node", select: {type: "node", nodeType: "heading", level: 2}}, args: {inline: {italic: true}}, require: "all"} +]}) +``` + + +Format targets must exist before the batch runs. Node selectors resolve at step execution time using the current document state. If a required target is missing, the step fails and — with `atomic: true` — the entire batch rolls back. + + +### When to use which tool + +| Task | Best tool | +|------|-----------| +| Insert one paragraph or heading | `superdoc_create` | +| Insert multiple sections with structure | `superdoc_edit` with `type: "markdown"` | +| Replace text across the whole document | `superdoc_search` + `superdoc_edit` | +| Rewrite a whole paragraph | `superdoc_get_content` + `superdoc_edit` with block ref | +| Apply formatting to known text | `superdoc_search` + `superdoc_format` | +| Apply formatting to all nodes of a type | `superdoc_mutations` with `format.apply` | +| Multiple text changes that must succeed together | `superdoc_mutations` with `atomic: true` | +| Review or resolve tracked changes | `superdoc_track_changes` | + ## Targeting Every editing tool needs a **target** telling the API *where* to apply the change. There are three ways to get one: - **From blocks data**: Each block has a `ref` (pass to `superdoc_edit` or `superdoc_format`) and a `nodeId` (for building `at` positions with `superdoc_create`). - **From `superdoc_search`**: Returns `handle.ref` covering the matched text. Use search when you need to find text patterns, not when you already know which block to target. -- **From `superdoc_create`**: Returns `nodeId` for chaining creates and building block targets. Re-fetch blocks after create to get a fresh ref before formatting. +- **From `superdoc_create`**: Returns `nodeId` and `ref` for the new node. For creating multiple sections or blocks at once, prefer `superdoc_edit` with `type: "markdown"` instead of multiple `superdoc_create` calls. Re-fetch blocks after create to get a fresh ref before formatting. -**Refs expire after any mutation.** Always re-search or re-read blocks before the next operation. +**Refs expire after any mutation.** Always re-search or re-read blocks before the next operation. Within a `superdoc_mutations` batch, node selectors resolve automatically at step execution time, so re-fetching between steps is not required. ## Common operations @@ -75,7 +130,7 @@ superdoc_list({session_id, action: "create", mode: "fromParagraphs", preset: "di ### Batch edits atomically -Use `superdoc_mutations` when you need multiple text changes that must succeed or fail together: +Use `superdoc_mutations` when you need multiple changes that must succeed or fail together. Supported step types include `text.rewrite`, `text.delete`, `format.apply`, `create.heading`, `create.paragraph`, and `create.table`. ``` superdoc_mutations({session_id, action: "apply", atomic: true, changeMode: "direct", steps: [ From 9f65e2c1a0915e7cd4a4f3b05cfc2d22ea69a74a Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 14:00:13 -0300 Subject: [PATCH 06/26] fix(evals): remove debug console.log that dumped every SDK message --- evals/providers/claude-code-agent.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/evals/providers/claude-code-agent.mjs b/evals/providers/claude-code-agent.mjs index c66e68fd1d..f6697ac603 100644 --- a/evals/providers/claude-code-agent.mjs +++ b/evals/providers/claude-code-agent.mjs @@ -249,7 +249,6 @@ export default class ClaudeCodeBenchmarkProvider { prompt: fullPrompt, options: queryOptions, })) { - console.log('message', message); if (message.type === 'assistant' && message.message?.content) { for (const block of message.message.content) { From 13ba3a6a98a648fe3a602862567147f1c50ff7d3 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 18:46:55 -0300 Subject: [PATCH 07/26] feat(document-api): add alignment field to StyleApplyStep and StyleApplyInput types --- packages/document-api/src/format/format.ts | 3 ++- packages/document-api/src/types/mutation-plan.types.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/document-api/src/format/format.ts b/packages/document-api/src/format/format.ts index 42619ad305..6471eef1be 100644 --- a/packages/document-api/src/format/format.ts +++ b/packages/document-api/src/format/format.ts @@ -62,7 +62,8 @@ export type FormatInlineAliasInput = K extends Impl export type StyleApplyInput = TargetLocator & { target?: SelectionTarget; ref?: string; - inline: InlineRunPatch; + inline?: InlineRunPatch; + alignment?: 'left' | 'center' | 'right' | 'justify'; /** Target a specific document story (body, header, footer, footnote, endnote). */ in?: StoryLocator; }; diff --git a/packages/document-api/src/types/mutation-plan.types.ts b/packages/document-api/src/types/mutation-plan.types.ts index f4e3b48f57..5b914b489c 100644 --- a/packages/document-api/src/types/mutation-plan.types.ts +++ b/packages/document-api/src/types/mutation-plan.types.ts @@ -117,7 +117,8 @@ export type StyleApplyStep = { op: 'format.apply'; where: StepWhere; args: { - inline: InlineRunPatch; + inline?: InlineRunPatch; + alignment?: 'left' | 'center' | 'right' | 'justify'; }; }; From b67b9091d4f76f5e0b63923515e565e287f8b9f5 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 18:48:00 -0300 Subject: [PATCH 08/26] fix(document-api): keep inline required on StyleApplyInput, guard optional inline in step executors --- packages/document-api/src/format/format.ts | 3 +- .../plan-engine/plan-wrappers.ts | 47 +++++++++++++++++-- .../plan-engine/register-executors.ts | 2 +- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/document-api/src/format/format.ts b/packages/document-api/src/format/format.ts index 6471eef1be..42619ad305 100644 --- a/packages/document-api/src/format/format.ts +++ b/packages/document-api/src/format/format.ts @@ -62,8 +62,7 @@ export type FormatInlineAliasInput = K extends Impl export type StyleApplyInput = TargetLocator & { target?: SelectionTarget; ref?: string; - inline?: InlineRunPatch; - alignment?: 'left' | 'center' | 'right' | 'justify'; + inline: InlineRunPatch; /** Target a specific document story (body, header, footer, footnote, endnote). */ in?: StoryLocator; }; diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/plan-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/plan-wrappers.ts index 7920f2ee78..0a072d8513 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/plan-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/plan-wrappers.ts @@ -503,8 +503,8 @@ export function styleApplyWrapper( }; } - const inlineKeys = Object.keys(input.inline) as InlineRunPatchKey[]; - ensureInlinePropertyCapabilities(editor, inlineKeys); + const inlineKeys = input.inline ? (Object.keys(input.inline) as InlineRunPatchKey[]) : []; + if (inlineKeys.length > 0) ensureInlinePropertyCapabilities(editor, inlineKeys); const mode = options?.changeMode ?? 'direct'; if (mode === 'tracked') { @@ -944,7 +944,14 @@ function insertStructuredInner(editor: Editor, input: InsertInput, options?: Mut // Legacy markdown/html path const contentType = input.type ?? 'text'; - const { value, target, ref } = input as { value: string; target?: SelectionTarget; ref?: string; type?: string }; + const { value, ref } = input as { value: string; ref?: string; type?: string }; + const rawTarget = (input as Record).target; + const placement = (input as Record).placement as + | 'before' + | 'after' + | 'insideStart' + | 'insideEnd' + | undefined; // Tracked mode not supported for structured content const mode = options?.changeMode ?? 'direct'; @@ -963,7 +970,35 @@ function insertStructuredInner(editor: Editor, input: InsertInput, options?: Mut throw new DocumentApiAdapterError('INVALID_TARGET', 'ref must be a non-empty string.', { ref }); } - if (target) { + // BlockNodeAddress target (markdown/html with block-level positioning) + if ( + rawTarget && + typeof rawTarget === 'object' && + 'kind' in rawTarget && + (rawTarget as BlockNodeAddress).kind === 'block' + ) { + const blockTarget = rawTarget as BlockNodeAddress; + let resolved; + try { + resolved = resolveStructuralInsertTarget(editor, blockTarget); + } catch (err) { + if (err instanceof DocumentApiAdapterError) throw err; + throw new DocumentApiAdapterError( + 'TARGET_NOT_FOUND', + `Cannot resolve insert target for block "${blockTarget.nodeId}".`, + ); + } + let insertPos: number; + if (resolved.targetNode && resolved.targetNodePos !== undefined) { + insertPos = resolvePlacement(editor.state.doc, resolved.targetNodePos, resolved.targetNode, placement); + } else { + insertPos = resolved.insertPos; + } + resolvedRange = { from: insertPos, to: insertPos }; + effectiveTarget = { kind: 'text', blockId: blockTarget.nodeId, range: { start: 0, end: 0 } }; + } else if (rawTarget) { + // SelectionTarget path + const target = rawTarget as SelectionTarget; const resolved = resolveSelectionTarget(editor, target); resolvedRange = { from: resolved.absFrom, to: resolved.absTo }; // Derive backward-compatible TextAddress from the start point @@ -1045,7 +1080,9 @@ function insertStructuredInner(editor: Editor, input: InsertInput, options?: Mut // Explicit targets with a non-collapsed range indicate a text selection — // that's a replace operation, not an insert. Refs are already collapsed // to their start position in the ref branch above. - if (target && from !== to) { + // BlockNodeAddress targets always produce collapsed ranges, so this only + // applies to SelectionTarget. + if (rawTarget && from !== to) { return { success: false, resolution, diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/register-executors.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/register-executors.ts index a6e034cc1f..a416429c63 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/register-executors.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/register-executors.ts @@ -263,7 +263,7 @@ function executeTextStep( } function ensureFormatStepCapabilities(ctx: ExecuteContext, step: StyleApplyStep): void { - const inlineKeys = Object.keys(step.args.inline) as InlineRunPatchKey[]; + const inlineKeys = step.args.inline ? (Object.keys(step.args.inline) as InlineRunPatchKey[]) : []; const capabilityIssue = getInlinePropertyCapabilityIssue(ctx.editor, inlineKeys, step.op); if (capabilityIssue) { throw planError(capabilityIssue.code, capabilityIssue.message, step.id, capabilityIssue.details); From 98a1609b85bedc175191754e71668a16cc98632c Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 18:48:45 -0300 Subject: [PATCH 09/26] feat(document-api): add alignment to format.apply step JSON schema --- packages/document-api/src/contract/schemas.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 94812164fc..2ed2cf2815 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -4872,8 +4872,14 @@ const operationSchemas: Record = { args: objectSchema( { inline: buildInlineRunPatchSchema(), + alignment: { + type: 'string', + enum: ['left', 'center', 'right', 'justify'], + description: + 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', + }, }, - ['inline'], + [], // Neither field is individually required — at least one must be present ), }, ['id', 'op', 'where', 'args'], From 69f45cf2b5885b6fc702862ed0caf61b4ca9d5a6 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 18:51:08 -0300 Subject: [PATCH 10/26] feat(super-editor): support alignment in format.apply mutation step --- .../plan-engine/executor.ts | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts index 696e17eb4e..92a76bd599 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts @@ -804,6 +804,43 @@ export function executeTextDelete( return { changed: true }; } +/** Alignment API value → OOXML justification value */ +const ALIGNMENT_TO_JUSTIFICATION: Record = { + left: 'left', + center: 'center', + right: 'right', + justify: 'both', +}; + +/** + * Applies alignment to the paragraph node(s) that contain the given range. + * Uses the same mechanism as paragraphsSetAlignmentWrapper: updates + * paragraphProperties.justification via tr.setNodeMarkup. + */ +function applyAlignmentToRange(tr: Transaction, absFrom: number, absTo: number, alignment: string): boolean { + const justification = ALIGNMENT_TO_JUSTIFICATION[alignment]; + if (!justification) return false; + + let changed = false; + const doc = tr.doc; + + doc.nodesBetween(absFrom, absTo, (node, pos) => { + // Only set alignment on textblock nodes (paragraphs, headings) + if (!node.isTextblock) return; + + const existing = (node.attrs as Record).paragraphProperties as Record | undefined; + const currentJustification = existing?.justification; + + if (currentJustification === justification) return; + + const updated = { ...(existing ?? {}), justification }; + tr.setNodeMarkup(pos, undefined, { ...node.attrs, paragraphProperties: updated }); + changed = true; + }); + + return changed; +} + export function executeStyleApply( editor: Editor, tr: Transaction, @@ -813,7 +850,18 @@ export function executeStyleApply( ): { changed: boolean } { const absFrom = mapping.map(target.absFrom); const absTo = mapping.map(target.absTo); - return { changed: applyInlinePatchToRange(editor, tr, absFrom, absTo, step.args.inline) }; + + let changed = false; + + if (step.args.inline) { + changed = applyInlinePatchToRange(editor, tr, absFrom, absTo, step.args.inline) || changed; + } + + if (step.args.alignment) { + changed = applyAlignmentToRange(tr, absFrom, absTo, step.args.alignment) || changed; + } + + return { changed }; } // --------------------------------------------------------------------------- @@ -957,7 +1005,17 @@ export function executeSpanStyleApply( const absFrom = mapping.map(firstSeg.absFrom, 1); const absTo = mapping.map(lastSeg.absTo, -1); - return { changed: applyInlinePatchToRange(editor, tr, absFrom, absTo, step.args.inline) }; + let changed = false; + + if (step.args.inline) { + changed = applyInlinePatchToRange(editor, tr, absFrom, absTo, step.args.inline) || changed; + } + + if (step.args.alignment) { + changed = applyAlignmentToRange(tr, absFrom, absTo, step.args.alignment) || changed; + } + + return { changed }; } // --------------------------------------------------------------------------- From 9aa655b6cc3015664f076c722b056ba46412deb0 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 18:53:09 -0300 Subject: [PATCH 11/26] docs(sdk): update tool descriptions to show alignment inside format.apply step --- .../src/contract/operation-definitions.ts | 36 +++++++++++-------- .../prompt-templates/system-prompt-core.md | 32 ++++++++++------- .../system-prompt-mcp-header.md | 31 ++++++++++------ 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 4ceaef39b0..2ba5b01101 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -144,28 +144,34 @@ export const INTENT_GROUP_META: Record = { edit: { toolName: 'superdoc_edit', description: + 'The primary tool for inserting content into documents. ' + + 'ALWAYS use action "insert" with type "markdown" to create headings, paragraphs, or any block content — this is faster and creates proper document structure in one call. Do NOT use superdoc_create for headings or paragraphs. ' + + 'The markdown parser creates headings from # markers (# = Heading1, ## = Heading2), bold from **text**, italic from *text*, and numbered/bullet lists. ' + + 'Position markdown inserts with "target" (a BlockNodeAddress like {kind:"block", nodeType, nodeId}) and "placement" (before, after, insideStart, insideEnd). Without a target, content appends at the end of the document. ' + + 'IMPORTANT: After a markdown insert, follow up with ONE superdoc_mutations call using format.apply steps to match the document style. ' + + 'Each format.apply step accepts "inline" (fontFamily, fontSize, bold, underline, color) AND "alignment" ("left","center","right","justify") in the same step — combine both in one step per block. ' + + 'Look at nearby headings and paragraphs in the get_content response and copy their exact formatting. Do NOT invent values — match what is already in the document. ' + + 'Also supports replace, delete, and undo/redo. For replace and delete, pass a "ref" from superdoc_search or superdoc_get_content blocks. ' + + 'A search ref covers only the matched substring; a block ref covers the entire block text, so use block refs when rewriting or shortening whole paragraphs. ' + 'Refs expire after any mutation; always re-search before the next edit. ' + - 'Modify document text: insert new content, replace existing text, delete a range, or undo/redo. ' + - 'Use this for single text modifications. For 2+ edits that must succeed or fail atomically, use superdoc_mutations instead. ' + - 'For replace and delete, pass a "ref" from superdoc_search or superdoc_get_content blocks. A search ref covers only the matched substring; a block ref covers the entire block text, so use block refs when rewriting or shortening whole paragraphs. ' + - 'Insert supports plain text (default), markdown, or html via the "type" parameter. ' + - 'To create a document with multiple headings and paragraphs, use action "insert" with type "markdown" and placement "end". ' + - 'The markdown parser creates proper Heading styles from # markers (# = Heading1, ## = Heading2), bold from **text**, italic from *text*, and numbered/bullet lists. ' + - 'This is the most efficient way to build document structure: one call creates all sections. Follow up with superdoc_mutations format.apply steps to apply formatting (color, font, alignment) that markdown cannot express. ' + - 'Use "placement" (before, after, insideStart, insideEnd) to control position relative to the target. ' + + 'For 2+ edits that must succeed or fail atomically, use superdoc_mutations instead. ' + 'Supports "dryRun" to preview changes and "changeMode: tracked" to record edits as tracked changes (not supported for markdown/html inserts). ' + 'Do NOT build "target" objects manually when a ref is available; prefer "ref" for simpler, more reliable targeting.', inputExamples: [ - { action: 'replace', ref: '', text: 'new text here' }, - { action: 'insert', value: 'Appended paragraph.', placement: 'insideEnd' }, - { action: 'insert', ref: '', value: 'Inserted before.', placement: 'before' }, { action: 'insert', type: 'markdown', - placement: 'end', + target: { kind: 'block', nodeType: 'paragraph', nodeId: '' }, + placement: 'before', + value: '# Executive Summary\n\nThis agreement sets forth the principal terms...', + }, + { + action: 'insert', + type: 'markdown', value: '# Section Title\n\nParagraph content here.\n\n# Another Section\n\nMore content with **bold** and *italic*.', }, + { action: 'replace', ref: '', text: 'new text here' }, { action: 'delete', ref: '' }, { action: 'undo' }, ], @@ -173,9 +179,9 @@ export const INTENT_GROUP_META: Record = { create: { toolName: 'superdoc_create', description: - 'Create a single paragraph, heading, or table. Returns nodeId and ref for the created block. ' + - 'For creating multiple headings and paragraphs at once, prefer superdoc_edit with type "markdown" (one call for all structure) instead of calling superdoc_create repeatedly. ' + - 'Use superdoc_create when you need to add one block at a specific position relative to another block. ' + + 'IMPORTANT: For headings and paragraphs, use superdoc_edit with type "markdown" instead — it is faster, creates proper styles, and handles positioning via target + placement. ' + + 'Only use superdoc_create for tables or when markdown cannot express the content. ' + + 'Creates a single paragraph, heading, or table. Returns nodeId and ref for the created block. ' + 'After creating, the returned ref is valid for ONE immediate superdoc_format call. For subsequent operations, re-fetch blocks with superdoc_get_content to get fresh refs (refs expire after any mutation). ' + 'When the user asks for a "heading", use action "heading" with a level (default 1). Use action "paragraph" for regular body text. ' + 'Position with "at": {kind:"documentEnd"} (default), {kind:"documentStart"}, or {kind:"after"/"before", target:{kind:"block", nodeType, nodeId}} for relative placement. ' + diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index 0ba82d9c2b..3c0303e2d5 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -128,29 +128,37 @@ Use preset "disc" for bullets, "decimal" for numbered. WARNING: the range conver 3. To change a bullet list to numbered: `superdoc_list({action: "set_type", target: {kind: "block", nodeType: "listItem", nodeId: ""}, kind: "ordered"})` -### Create a multi-section document efficiently +### Insert content into a document (new or existing) -**Step 1: Create all structure in one call using markdown insert:** +Markdown insert creates block structure but uses default formatting. You MUST follow up with formatting to match the document's existing style. + +**Step 1: Read existing formatting** from the initial get_content blocks response. Pay special attention to: +- **Nearby headings/titles**: look at their fontFamily, fontSize, bold, underline, alignment (especially center vs justify vs left). Your new headings must match these exactly. +- **Body paragraphs**: note fontFamily, fontSize, color, alignment. Your new paragraphs must match. +- Match the style of the nearest similar element, not arbitrary values. + +**Step 2: Insert content with markdown:** ``` -superdoc_edit({action: "insert", type: "markdown", placement: "end", - value: "# Definitions\n\n\"Confidential Information\" means any non-public information...\n\n# Obligations\n\nThe Receiving Party agrees to maintain confidentiality...\n\n# Governing Law\n\nThis Agreement shall be governed by the laws of..."}) +superdoc_edit({action: "insert", type: "markdown", + target: {kind: "block", nodeType: "paragraph", nodeId: ""}, + placement: "before", + value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."}) ``` -This creates headings with proper Heading1 style, paragraphs, bold (**text**), italic (*text*), lists, and tables in one call. Markdown cannot express color, font-size, or alignment — those require step 2. - -**Step 2: Apply formatting in one batch:** +**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts both `inline` (text styles) AND `alignment` (paragraph alignment) — one step per block. +Example: if the document has centered, underlined, 12pt headings and justified 12pt body text: ``` -superdoc_mutations({action: "apply", steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "node", nodeType: "heading"}, require: "all"}, args: {inline: {color: "#FF0000"}}}, - {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Confidential Information"}, require: "all"}, args: {inline: {bold: true}}} +superdoc_mutations({action: "apply", atomic: true, steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify"}} ]}) ``` -Use `require: "all"` with a node selector to format every heading at once instead of formatting one at a time. +CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use ONE format.apply step per block with both `inline` and `alignment` combined. -Total: 4 calls (open, insert, format batch, save) instead of 40+. +Total: 3 calls (read + insert + format-all-in-one-batch). Never more. ### Batch multiple text edits atomically diff --git a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md index d980bf2b28..bb27e8f478 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md @@ -14,27 +14,36 @@ These tools handle the OOXML format correctly and preserve document structure. ## Efficient patterns (use these instead of calling tools one at a time) -**Creating multiple headings and paragraphs — use markdown insert (one call):** +**Creating headings and paragraphs — ALWAYS use markdown insert (one call):** ``` -superdoc_edit({action: "insert", type: "markdown", placement: "end", +superdoc_edit({action: "insert", type: "markdown", value: "# Section Title\n\nParagraph content.\n\n# Another Section\n\nMore content with **bold**."}) ``` This creates proper Heading styles from # markers. One call replaces many superdoc_create calls. -**Formatting multiple items at once — use mutations batch (one call):** +**Inserting at a specific position — use target + placement:** ``` -superdoc_mutations({action: "apply", steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "node", nodeType: "heading"}, require: "all"}, args: {inline: {color: "#FF0000"}}}, - {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "important term"}, require: "all"}, args: {inline: {bold: true}}} +superdoc_edit({action: "insert", type: "markdown", + target: {kind: "block", nodeType: "paragraph", nodeId: ""}, + placement: "before", + value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."}) +``` +Valid placements: "before", "after", "insideStart", "insideEnd". Without target, content appends at document end. + +**Formatting — each format.apply step accepts both `inline` AND `alignment`:** +``` +superdoc_mutations({action: "apply", atomic: true, steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "body paragraph text"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify"}} ]}) ``` -Use require "all" to format every match at once. Selectors resolve before execution, so format targets must exist in the document before the batch runs. +One format.apply step per block. Combine `inline` (text styles) and `alignment` (paragraph alignment) in the same step. Do NOT use separate superdoc_format calls. **When to use which tool:** -- Creating multiple blocks → `superdoc_edit` with type "markdown" -- Creating one block at a specific position → `superdoc_create` -- Formatting multiple items → `superdoc_mutations` with format.apply steps -- Formatting one item → `superdoc_format` +- Creating headings, paragraphs, or any block content → `superdoc_edit` with type "markdown" (preferred, even for a single heading + paragraph) +- Creating one block only when markdown is insufficient → `superdoc_create` +- ALL formatting after insert → `superdoc_mutations` with format.apply (inline + alignment in one step per block) +- Single quick format (no insert before it) → `superdoc_format` - Multiple text edits → `superdoc_mutations` - Single text edit → `superdoc_edit` From 145693d72cb04435719cc406f29d70d4e1a85323 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:03:41 -0300 Subject: [PATCH 12/26] feat(document-api): add scope: block to format.apply for full-paragraph formatting --- packages/document-api/src/contract/schemas.ts | 8 +++- .../src/types/mutation-plan.types.ts | 2 + .../prompt-templates/system-prompt-core.md | 10 +++-- .../system-prompt-mcp-header.md | 8 ++-- .../plan-engine/executor.ts | 44 +++++++++++++++++-- 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 2ed2cf2815..5b4c8d7155 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -4878,8 +4878,14 @@ const operationSchemas: Record = { description: 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', }, + scope: { + type: 'string', + enum: ['match', 'block'], + description: + 'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".', + }, }, - [], // Neither field is individually required — at least one must be present + [], // No individual field is required — at least one must be present ), }, ['id', 'op', 'where', 'args'], diff --git a/packages/document-api/src/types/mutation-plan.types.ts b/packages/document-api/src/types/mutation-plan.types.ts index 5b914b489c..b6cbf9a2bc 100644 --- a/packages/document-api/src/types/mutation-plan.types.ts +++ b/packages/document-api/src/types/mutation-plan.types.ts @@ -119,6 +119,8 @@ export type StyleApplyStep = { args: { inline?: InlineRunPatch; alignment?: 'left' | 'center' | 'right' | 'justify'; + /** When "block", inline formatting expands to cover the entire parent textblock(s), not just the matched range. Default: "match". */ + scope?: 'match' | 'block'; }; }; diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index 3c0303e2d5..e049c08398 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -146,17 +146,19 @@ superdoc_edit({action: "insert", type: "markdown", value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."}) ``` -**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts both `inline` (text styles) AND `alignment` (paragraph alignment) — one step per block. +**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts `inline` (text styles), `alignment` (paragraph alignment), and `scope` — combine them all in one step per block. + +ALWAYS use `scope: "block"` after markdown inserts. This makes the formatting cover the entire paragraph, not just the matched text pattern. The pattern only needs to uniquely identify which paragraph — a short prefix is enough. Example: if the document has centered, underlined, 12pt headings and justified 12pt body text: ``` superdoc_mutations({action: "apply", atomic: true, steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center"}}, - {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify"}} + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}} ]}) ``` -CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use ONE format.apply step per block with both `inline` and `alignment` combined. +CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use `scope: "block"` so the formatting covers the ENTIRE paragraph, not just the matched text. Total: 3 calls (read + insert + format-all-in-one-batch). Never more. diff --git a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md index bb27e8f478..f635a68653 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md @@ -30,14 +30,14 @@ superdoc_edit({action: "insert", type: "markdown", ``` Valid placements: "before", "after", "insideStart", "insideEnd". Without target, content appends at document end. -**Formatting — each format.apply step accepts both `inline` AND `alignment`:** +**Formatting — use `scope: "block"` to format entire paragraphs after markdown insert:** ``` superdoc_mutations({action: "apply", atomic: true, steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center"}}, - {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "body paragraph text"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify"}} + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify", scope: "block"}} ]}) ``` -One format.apply step per block. Combine `inline` (text styles) and `alignment` (paragraph alignment) in the same step. Do NOT use separate superdoc_format calls. +One format.apply step per block. Combine `inline`, `alignment`, and `scope: "block"` in each step. The pattern only needs to identify which paragraph — `scope: "block"` formats the entire paragraph, not just the matched text. **When to use which tool:** - Creating headings, paragraphs, or any block content → `superdoc_edit` with type "markdown" (preferred, even for a single heading + paragraph) diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts index 92a76bd599..9fa7901eca 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts @@ -841,6 +841,29 @@ function applyAlignmentToRange(tr: Transaction, absFrom: number, absTo: number, return changed; } +/** + * Expands a position range to cover the full content of all textblock nodes + * that overlap with it. Used when scope: "block" is set on a format.apply step. + */ +function expandToBlockBoundaries( + doc: import('prosemirror-model').Node, + from: number, + to: number, +): { from: number; to: number } { + let expandedFrom = from; + let expandedTo = to; + + doc.nodesBetween(from, to, (node, pos) => { + if (!node.isTextblock) return; + const blockContentStart = pos + 1; + const blockContentEnd = pos + node.nodeSize - 1; + expandedFrom = Math.min(expandedFrom, blockContentStart); + expandedTo = Math.max(expandedTo, blockContentEnd); + }); + + return { from: expandedFrom, to: expandedTo }; +} + export function executeStyleApply( editor: Editor, tr: Transaction, @@ -848,8 +871,15 @@ export function executeStyleApply( step: StyleApplyStep, mapping: Mapping, ): { changed: boolean } { - const absFrom = mapping.map(target.absFrom); - const absTo = mapping.map(target.absTo); + let absFrom = mapping.map(target.absFrom); + let absTo = mapping.map(target.absTo); + + // Expand to full block boundaries when scope is "block" + if (step.args.scope === 'block') { + const expanded = expandToBlockBoundaries(tr.doc, absFrom, absTo); + absFrom = expanded.from; + absTo = expanded.to; + } let changed = false; @@ -1002,8 +1032,14 @@ export function executeSpanStyleApply( // Apply marks uniformly across the full span const firstSeg = target.segments[0]; const lastSeg = target.segments[target.segments.length - 1]; - const absFrom = mapping.map(firstSeg.absFrom, 1); - const absTo = mapping.map(lastSeg.absTo, -1); + let absFrom = mapping.map(firstSeg.absFrom, 1); + let absTo = mapping.map(lastSeg.absTo, -1); + + if (step.args.scope === 'block') { + const expanded = expandToBlockBoundaries(tr.doc, absFrom, absTo); + absFrom = expanded.from; + absTo = expanded.to; + } let changed = false; From 64cab8749fe5f9a4c5b4ba3565937d70e7e69ebf Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:08:18 -0300 Subject: [PATCH 13/26] feat(document-api): allow placement and BlockNodeAddress target for markdown inserts --- examples/collaboration/ai-node-sdk/Makefile | 10 ++--- packages/document-api/src/index.test.ts | 17 +++++++- packages/document-api/src/insert/insert.ts | 43 +++++++++++++++++---- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/examples/collaboration/ai-node-sdk/Makefile b/examples/collaboration/ai-node-sdk/Makefile index 1b8bc12a26..391570cf37 100644 --- a/examples/collaboration/ai-node-sdk/Makefile +++ b/examples/collaboration/ai-node-sdk/Makefile @@ -111,13 +111,9 @@ restore-npm-sdk: ## Restore SDK from npm if currently symlinked link-local-sdk: ## Build and link local SuperDoc SDK + CLI @echo "Linking local SuperDoc SDK ($(CLI_TARGET))..." - @# 1. Build CLI native binary if not already built - @if [ ! -f $(CLI_ARTIFACT) ]; then \ - echo " Building CLI binary..."; \ - cd $(ROOT) && $(PNPM) --filter @superdoc-dev/cli run build:native:host; \ - else \ - echo " CLI binary found"; \ - fi + @# 1. Always rebuild CLI binary to pick up latest source changes + @echo " Building CLI binary..." + @cd $(ROOT) && $(PNPM) --filter @superdoc-dev/cli run build:native:host @# 2. Ensure server node_modules exists @if [ ! -d server/node_modules ]; then \ echo " Installing server deps first..."; \ diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index 29b1f65edf..d03716a034 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -1454,10 +1454,23 @@ describe('createDocumentApi', () => { ); }); - it('rejects legacy insert with structural "placement" field', () => { + it('rejects plain text insert with structural "placement" field', () => { const api = makeApi(); expect(() => api.insert({ value: 'hi', placement: 'before' } as any)).toThrow( - /"placement" is only valid with structural/, + /"placement" is only valid with structural content input or markdown\/html/, + ); + }); + + it('accepts placement for markdown insert', () => { + const api = makeApi(); + // Should not throw — markdown inserts route through the structural path + expect(() => api.insert({ value: '# Hello', type: 'markdown', placement: 'insideEnd' } as any)).not.toThrow(); + }); + + it('rejects invalid placement value for markdown insert', () => { + const api = makeApi(); + expect(() => api.insert({ value: '# Hello', type: 'markdown', placement: 'end' } as any)).toThrow( + /placement must be one of/, ); }); diff --git a/packages/document-api/src/insert/insert.ts b/packages/document-api/src/insert/insert.ts index 91cf785d89..9dec1a62ed 100644 --- a/packages/document-api/src/insert/insert.ts +++ b/packages/document-api/src/insert/insert.ts @@ -59,7 +59,7 @@ export type InsertInput = TextInsertInput | SDInsertInput; // Allowlists for strict field validation // --------------------------------------------------------------------------- -const TEXT_INSERT_ALLOWED_KEYS = new Set(['value', 'type', 'target', 'ref', 'in']); +const TEXT_INSERT_ALLOWED_KEYS = new Set(['value', 'type', 'target', 'ref', 'in', 'placement']); const STRUCTURAL_INSERT_ALLOWED_KEYS = new Set(['content', 'target', 'placement', 'nestingPolicy', 'in']); const VALID_INSERT_TYPES: ReadonlySet = new Set(['text', 'markdown', 'html']); @@ -121,11 +121,15 @@ function validateInsertInput(input: unknown): asserts input is InsertInput { /** Validates the text-based insert input shape. */ function validateTextInsertInput(input: Record): void { + const contentType = typeof input.type === 'string' ? input.type : 'text'; + const isRichContent = contentType === 'markdown' || contentType === 'html'; + // Union conflict rule 4: structural-only fields with text shape - if ('placement' in input && input.placement !== undefined) { + // placement is allowed for markdown/html since they route through the structural path + if ('placement' in input && input.placement !== undefined && !isRichContent) { throw new DocumentApiValidationError( 'INVALID_INPUT', - '"placement" is only valid with structural content input, not with "value".', + '"placement" is only valid with structural content input or markdown/html inserts, not with plain "value".', { field: 'placement' }, ); } @@ -139,6 +143,17 @@ function validateTextInsertInput(input: Record): void { assertNoUnknownFields(input, TEXT_INSERT_ALLOWED_KEYS, 'insert'); + // Validate placement value when provided for markdown/html + if (isRichContent && 'placement' in input && input.placement !== undefined) { + if (typeof input.placement !== 'string' || !PLACEMENT_VALUES.has(input.placement)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `placement must be one of: before, after, insideStart, insideEnd. Got "${String(input.placement)}".`, + { field: 'placement', value: input.placement }, + ); + } + } + const { target, ref, value, type } = input; // Mutual exclusivity: target and ref @@ -150,11 +165,23 @@ function validateTextInsertInput(input: Record): void { ); } - if (target !== undefined && !isSelectionTarget(target)) { - throw new DocumentApiValidationError('INVALID_TARGET', 'target must be a SelectionTarget object.', { - field: 'target', - value: target, - }); + if (target !== undefined) { + // Markdown/html inserts accept BlockNodeAddress targets (with optional placement) + // since they route through the structural insert path and produce block-level content. + if (isRichContent) { + if (!isSelectionTarget(target) && !isBlockNodeAddress(target)) { + throw new DocumentApiValidationError( + 'INVALID_TARGET', + 'target must be a SelectionTarget or BlockNodeAddress for markdown/html inserts.', + { field: 'target', value: target }, + ); + } + } else if (!isSelectionTarget(target)) { + throw new DocumentApiValidationError('INVALID_TARGET', 'target must be a SelectionTarget object.', { + field: 'target', + value: target, + }); + } } if (ref !== undefined && (typeof ref !== 'string' || ref === '')) { From c3395d634be685924c665bb37dab3cc3f4623337 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:08:26 -0300 Subject: [PATCH 14/26] chore: regenerate SDK artifacts and docs from updated contract --- .../reference/_generated-manifest.json | 2 +- .../reference/mutations/apply.mdx | 21 ++++- .../reference/mutations/preview.mdx | 21 ++++- .../sdk/langs/browser/src/system-prompt.ts | 42 ++++++++- packages/sdk/tools/system-prompt-mcp.md | 87 ++++++++++++++++--- packages/sdk/tools/system-prompt.md | 42 ++++++++- 6 files changed, 189 insertions(+), 26 deletions(-) diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index fe07ca9e55..51cfa33df2 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -1016,5 +1016,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "4a3601ee0f28a73c712fbe06e8b4913a9ae882a71152f9f6e892ea51137fc5e8" + "sourceHash": "a77f6a56704f39dcdc2d65ef8192ea4fffc0c8fb797bf13f516a949557d6e0ec" } diff --git a/apps/docs/document-api/reference/mutations/apply.mdx b/apps/docs/document-api/reference/mutations/apply.mdx index e6131bdc24..ff8f26083b 100644 --- a/apps/docs/document-api/reference/mutations/apply.mdx +++ b/apps/docs/document-api/reference/mutations/apply.mdx @@ -946,6 +946,16 @@ The runtime capability snapshot also exposes this allowlist at `planEngine.suppo "args": { "additionalProperties": false, "properties": { + "alignment": { + "description": "Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.", + "enum": [ + "left", + "center", + "right", + "justify" + ], + "type": "string" + }, "inline": { "additionalProperties": false, "minProperties": 1, @@ -1754,11 +1764,16 @@ The runtime capability snapshot also exposes this allowlist at `planEngine.suppo } }, "type": "object" + }, + "scope": { + "description": "When \"block\", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use \"block\" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: \"match\".", + "enum": [ + "match", + "block" + ], + "type": "string" } }, - "required": [ - "inline" - ], "type": "object" }, "id": { diff --git a/apps/docs/document-api/reference/mutations/preview.mdx b/apps/docs/document-api/reference/mutations/preview.mdx index 391a72fcb9..ab99138611 100644 --- a/apps/docs/document-api/reference/mutations/preview.mdx +++ b/apps/docs/document-api/reference/mutations/preview.mdx @@ -932,6 +932,16 @@ The runtime capability snapshot also exposes this allowlist at `planEngine.suppo "args": { "additionalProperties": false, "properties": { + "alignment": { + "description": "Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.", + "enum": [ + "left", + "center", + "right", + "justify" + ], + "type": "string" + }, "inline": { "additionalProperties": false, "minProperties": 1, @@ -1740,11 +1750,16 @@ The runtime capability snapshot also exposes this allowlist at `planEngine.suppo } }, "type": "object" + }, + "scope": { + "description": "When \"block\", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use \"block\" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: \"match\".", + "enum": [ + "match", + "block" + ], + "type": "string" } }, - "required": [ - "inline" - ], "type": "object" }, "id": { diff --git a/packages/sdk/langs/browser/src/system-prompt.ts b/packages/sdk/langs/browser/src/system-prompt.ts index b9e2ddc6dd..1df4c6817e 100644 --- a/packages/sdk/langs/browser/src/system-prompt.ts +++ b/packages/sdk/langs/browser/src/system-prompt.ts @@ -24,9 +24,9 @@ Every editing tool needs a **target** telling the API *where* to apply the chang - **From blocks data**: Each block has a \`ref\` (pass directly to superdoc_edit or superdoc_format) and a \`nodeId\` (for building \`at\` positions with superdoc_create). - **From superdoc_search**: Returns \`handle.ref\` covering the matched text. Use search when you need to find text patterns, not when you already know which block to target. -- **From superdoc_create**: Returns \`nodeId\` for chaining creates and building block targets. Re-fetch blocks after create to get a fresh ref before formatting. +- **From superdoc_create**: Returns \`nodeId\` and \`ref\`. The ref is valid for one immediate format call. For subsequent operations, re-fetch blocks to get fresh refs. -**Refs expire after any mutation.** Always re-search or re-read blocks before the next operation. +**Refs expire after any mutation** between separate tool calls. Within a superdoc_mutations batch, selectors resolve automatically — no manual re-searching between steps. ## Common workflows @@ -134,9 +134,43 @@ Use preset "disc" for bullets, "decimal" for numbered. WARNING: the range conver 3. To change a bullet list to numbered: \`superdoc_list({action: "set_type", target: {kind: "block", nodeType: "listItem", nodeId: ""}, kind: "ordered"})\` +### Insert content into a document (new or existing) + +Markdown insert creates block structure but uses default formatting. You MUST follow up with formatting to match the document's existing style. + +**Step 1: Read existing formatting** from the initial get_content blocks response. Pay special attention to: +- **Nearby headings/titles**: look at their fontFamily, fontSize, bold, underline, alignment (especially center vs justify vs left). Your new headings must match these exactly. +- **Body paragraphs**: note fontFamily, fontSize, color, alignment. Your new paragraphs must match. +- Match the style of the nearest similar element, not arbitrary values. + +**Step 2: Insert content with markdown:** + +\`\`\` +superdoc_edit({action: "insert", type: "markdown", + target: {kind: "block", nodeType: "paragraph", nodeId: ""}, + placement: "before", + value: "# Executive Summary\\n\\nThis agreement sets forth the principal terms..."}) +\`\`\` + +**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts \`inline\` (text styles), \`alignment\` (paragraph alignment), and \`scope\` — combine them all in one step per block. + +ALWAYS use \`scope: "block"\` after markdown inserts. This makes the formatting cover the entire paragraph, not just the matched text pattern. The pattern only needs to uniquely identify which paragraph — a short prefix is enough. + +Example: if the document has centered, underlined, 12pt headings and justified 12pt body text: +\`\`\` +superdoc_mutations({action: "apply", atomic: true, steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}} +]}) +\`\`\` + +CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use \`scope: "block"\` so the formatting covers the ENTIRE paragraph, not just the matched text. + +Total: 3 calls (read + insert + format-all-in-one-batch). Never more. + ### Batch multiple text edits atomically -Use superdoc_mutations when you need 2+ text changes that must succeed or fail together: +Use superdoc_mutations for 2+ text changes, format changes, or a combination: \`\`\` superdoc_mutations({ @@ -149,7 +183,7 @@ superdoc_mutations({ }) \`\`\` -Split mutations by phase: text mutations (text.rewrite, text.insert, text.delete) in one call, then formatting (format.apply) in a separate call with fresh refs from a new superdoc_search. +Selectors resolve at compile time (before execution). This means format.apply steps CANNOT target content created by create steps in the same batch — the new content does not exist yet when selectors compile. Split creates and formatting into separate batches. Never create two steps targeting overlapping text in the same block. Combine them into a single text.rewrite instead. diff --git a/packages/sdk/tools/system-prompt-mcp.md b/packages/sdk/tools/system-prompt-mcp.md index 7efbefc492..c1df473d03 100644 --- a/packages/sdk/tools/system-prompt-mcp.md +++ b/packages/sdk/tools/system-prompt-mcp.md @@ -6,15 +6,46 @@ These tools handle the OOXML format correctly and preserve document structure. ## Session lifecycle -Every interaction requires a session. Follow this workflow: +1. `superdoc_open({path: "/path/to/file.docx"})` — returns `session_id`. Opening a non-existent path creates a blank document. +2. Pass `session_id` to every subsequent tool call. +3. Read, edit, format the document using the tools below. +4. `superdoc_save({session_id})` — writes changes to disk. +5. `superdoc_close({session_id})` — releases the session. Always close when done. -1. `superdoc_open({file: "/path/to/file.docx"})` — returns `session_id` -2. Pass `session_id` to every subsequent tool call -3. Use intent tools (superdoc_search, superdoc_edit, etc.) to read and modify content -4. `superdoc_save({session_id})` — writes changes to disk (optional `out` for save-as) -5. `superdoc_close({session_id})` — releases the session +## Efficient patterns (use these instead of calling tools one at a time) -Opening a non-existent path creates a blank document. Always close sessions when done. +**Creating headings and paragraphs — ALWAYS use markdown insert (one call):** +``` +superdoc_edit({action: "insert", type: "markdown", + value: "# Section Title\n\nParagraph content.\n\n# Another Section\n\nMore content with **bold**."}) +``` +This creates proper Heading styles from # markers. One call replaces many superdoc_create calls. + +**Inserting at a specific position — use target + placement:** +``` +superdoc_edit({action: "insert", type: "markdown", + target: {kind: "block", nodeType: "paragraph", nodeId: ""}, + placement: "before", + value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."}) +``` +Valid placements: "before", "after", "insideStart", "insideEnd". Without target, content appends at document end. + +**Formatting — use `scope: "block"` to format entire paragraphs after markdown insert:** +``` +superdoc_mutations({action: "apply", atomic: true, steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify", scope: "block"}} +]}) +``` +One format.apply step per block. Combine `inline`, `alignment`, and `scope: "block"` in each step. The pattern only needs to identify which paragraph — `scope: "block"` formats the entire paragraph, not just the matched text. + +**When to use which tool:** +- Creating headings, paragraphs, or any block content → `superdoc_edit` with type "markdown" (preferred, even for a single heading + paragraph) +- Creating one block only when markdown is insufficient → `superdoc_create` +- ALL formatting after insert → `superdoc_mutations` with format.apply (inline + alignment in one step per block) +- Single quick format (no insert before it) → `superdoc_format` +- Multiple text edits → `superdoc_mutations` +- Single text edit → `superdoc_edit` ## Tools overview @@ -36,9 +67,9 @@ Every editing tool needs a **target** telling the API *where* to apply the chang - **From blocks data**: Each block has a `ref` (pass directly to superdoc_edit or superdoc_format) and a `nodeId` (for building `at` positions with superdoc_create). - **From superdoc_search**: Returns `handle.ref` covering the matched text. Use search when you need to find text patterns, not when you already know which block to target. -- **From superdoc_create**: Returns `nodeId` for chaining creates and building block targets. Re-fetch blocks after create to get a fresh ref before formatting. +- **From superdoc_create**: Returns `nodeId` and `ref`. The ref is valid for one immediate format call. For subsequent operations, re-fetch blocks to get fresh refs. -**Refs expire after any mutation.** Always re-search or re-read blocks before the next operation. +**Refs expire after any mutation** between separate tool calls. Within a superdoc_mutations batch, selectors resolve automatically — no manual re-searching between steps. ## Common workflows @@ -146,9 +177,43 @@ Use preset "disc" for bullets, "decimal" for numbered. WARNING: the range conver 3. To change a bullet list to numbered: `superdoc_list({action: "set_type", target: {kind: "block", nodeType: "listItem", nodeId: ""}, kind: "ordered"})` +### Insert content into a document (new or existing) + +Markdown insert creates block structure but uses default formatting. You MUST follow up with formatting to match the document's existing style. + +**Step 1: Read existing formatting** from the initial get_content blocks response. Pay special attention to: +- **Nearby headings/titles**: look at their fontFamily, fontSize, bold, underline, alignment (especially center vs justify vs left). Your new headings must match these exactly. +- **Body paragraphs**: note fontFamily, fontSize, color, alignment. Your new paragraphs must match. +- Match the style of the nearest similar element, not arbitrary values. + +**Step 2: Insert content with markdown:** + +``` +superdoc_edit({action: "insert", type: "markdown", + target: {kind: "block", nodeType: "paragraph", nodeId: ""}, + placement: "before", + value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."}) +``` + +**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts `inline` (text styles), `alignment` (paragraph alignment), and `scope` — combine them all in one step per block. + +ALWAYS use `scope: "block"` after markdown inserts. This makes the formatting cover the entire paragraph, not just the matched text pattern. The pattern only needs to uniquely identify which paragraph — a short prefix is enough. + +Example: if the document has centered, underlined, 12pt headings and justified 12pt body text: +``` +superdoc_mutations({action: "apply", atomic: true, steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}} +]}) +``` + +CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use `scope: "block"` so the formatting covers the ENTIRE paragraph, not just the matched text. + +Total: 3 calls (read + insert + format-all-in-one-batch). Never more. + ### Batch multiple text edits atomically -Use superdoc_mutations when you need 2+ text changes that must succeed or fail together: +Use superdoc_mutations for 2+ text changes, format changes, or a combination: ``` superdoc_mutations({ @@ -161,7 +226,7 @@ superdoc_mutations({ }) ``` -Split mutations by phase: text mutations (text.rewrite, text.insert, text.delete) in one call, then formatting (format.apply) in a separate call with fresh refs from a new superdoc_search. +Selectors resolve at compile time (before execution). This means format.apply steps CANNOT target content created by create steps in the same batch — the new content does not exist yet when selectors compile. Split creates and formatting into separate batches. Never create two steps targeting overlapping text in the same block. Combine them into a single text.rewrite instead. diff --git a/packages/sdk/tools/system-prompt.md b/packages/sdk/tools/system-prompt.md index 77185b14c8..e3fc3a9e33 100644 --- a/packages/sdk/tools/system-prompt.md +++ b/packages/sdk/tools/system-prompt.md @@ -22,9 +22,9 @@ Every editing tool needs a **target** telling the API *where* to apply the chang - **From blocks data**: Each block has a `ref` (pass directly to superdoc_edit or superdoc_format) and a `nodeId` (for building `at` positions with superdoc_create). - **From superdoc_search**: Returns `handle.ref` covering the matched text. Use search when you need to find text patterns, not when you already know which block to target. -- **From superdoc_create**: Returns `nodeId` for chaining creates and building block targets. Re-fetch blocks after create to get a fresh ref before formatting. +- **From superdoc_create**: Returns `nodeId` and `ref`. The ref is valid for one immediate format call. For subsequent operations, re-fetch blocks to get fresh refs. -**Refs expire after any mutation.** Always re-search or re-read blocks before the next operation. +**Refs expire after any mutation** between separate tool calls. Within a superdoc_mutations batch, selectors resolve automatically — no manual re-searching between steps. ## Common workflows @@ -132,9 +132,43 @@ Use preset "disc" for bullets, "decimal" for numbered. WARNING: the range conver 3. To change a bullet list to numbered: `superdoc_list({action: "set_type", target: {kind: "block", nodeType: "listItem", nodeId: ""}, kind: "ordered"})` +### Insert content into a document (new or existing) + +Markdown insert creates block structure but uses default formatting. You MUST follow up with formatting to match the document's existing style. + +**Step 1: Read existing formatting** from the initial get_content blocks response. Pay special attention to: +- **Nearby headings/titles**: look at their fontFamily, fontSize, bold, underline, alignment (especially center vs justify vs left). Your new headings must match these exactly. +- **Body paragraphs**: note fontFamily, fontSize, color, alignment. Your new paragraphs must match. +- Match the style of the nearest similar element, not arbitrary values. + +**Step 2: Insert content with markdown:** + +``` +superdoc_edit({action: "insert", type: "markdown", + target: {kind: "block", nodeType: "paragraph", nodeId: ""}, + placement: "before", + value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."}) +``` + +**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts `inline` (text styles), `alignment` (paragraph alignment), and `scope` — combine them all in one step per block. + +ALWAYS use `scope: "block"` after markdown inserts. This makes the formatting cover the entire paragraph, not just the matched text pattern. The pattern only needs to uniquely identify which paragraph — a short prefix is enough. + +Example: if the document has centered, underlined, 12pt headings and justified 12pt body text: +``` +superdoc_mutations({action: "apply", atomic: true, steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}} +]}) +``` + +CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use `scope: "block"` so the formatting covers the ENTIRE paragraph, not just the matched text. + +Total: 3 calls (read + insert + format-all-in-one-batch). Never more. + ### Batch multiple text edits atomically -Use superdoc_mutations when you need 2+ text changes that must succeed or fail together: +Use superdoc_mutations for 2+ text changes, format changes, or a combination: ``` superdoc_mutations({ @@ -147,7 +181,7 @@ superdoc_mutations({ }) ``` -Split mutations by phase: text mutations (text.rewrite, text.insert, text.delete) in one call, then formatting (format.apply) in a separate call with fresh refs from a new superdoc_search. +Selectors resolve at compile time (before execution). This means format.apply steps CANNOT target content created by create steps in the same batch — the new content does not exist yet when selectors compile. Split creates and formatting into separate batches. Never create two steps targeting overlapping text in the same block. Combine them into a single text.rewrite instead. From 1e4e294149c76ea2b636c55940cf94edd70f822e Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:18:45 -0300 Subject: [PATCH 15/26] feat(evals): add new NDA documents and implement interactive DOCX output reviewer --- evals/fixtures/claude-orlov-gs-nda.docx | Bin 0 -> 14545 bytes evals/fixtures/codex-orlov-gs-nda-codex.docx | Bin 0 -> 14196 bytes evals/fixtures/codex-orlov-gs-nda.docx | Bin 0 -> 14325 bytes evals/providers/claude-code-agent.mjs | 1 + evals/scripts/review-docx-outputs.mjs | 169 ++++++++++++++++++ .../src/components/chat/suggestion-chips.tsx | 4 + 6 files changed, 174 insertions(+) create mode 100644 evals/fixtures/claude-orlov-gs-nda.docx create mode 100644 evals/fixtures/codex-orlov-gs-nda-codex.docx create mode 100644 evals/fixtures/codex-orlov-gs-nda.docx create mode 100644 evals/scripts/review-docx-outputs.mjs diff --git a/evals/fixtures/claude-orlov-gs-nda.docx b/evals/fixtures/claude-orlov-gs-nda.docx new file mode 100644 index 0000000000000000000000000000000000000000..b6a4cecf20173b9967cf63948ca443b088db085e GIT binary patch literal 14545 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 Yds#V2&_Cu7(tEIp!u#|wXZ{KKAB{&4kN^Mx literal 0 HcmV?d00001 diff --git a/evals/fixtures/codex-orlov-gs-nda-codex.docx b/evals/fixtures/codex-orlov-gs-nda-codex.docx new file mode 100644 index 0000000000000000000000000000000000000000..188196c9c83a3bc4cad40087db2c79785735f4f7 GIT binary patch literal 14196 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 literal 0 HcmV?d00001 diff --git a/evals/fixtures/codex-orlov-gs-nda.docx b/evals/fixtures/codex-orlov-gs-nda.docx new file mode 100644 index 0000000000000000000000000000000000000000..1c894757fbe1dbda2239dbd888a91073fefba003 GIT binary patch literal 14325 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 { + const order = { 'superdoc-skill': 0, 'superdoc-cli': 1, 'raw': 2, 'vendor-skill': 3 }; + return (order[a.path] ?? 4) - (order[b.path] ?? 4); + }); + + for (const f of taskFiles) { + fileIndex++; + const guide = getGuide(f.task); + const guideText = guide.map((g, i) => `${i + 1}. ${g}`).join('\\n'); + + const title = `[${fileIndex}/${total}] ${f.status} — ${f.provider}`; + const message = [ + `Task: ${f.taskShort}`, + `Provider: ${f.provider}`, + `Status: ${f.status} | Path: ${f.path} | Steps: ${f.steps}`, + ``, + `What to look for:`, + ...guide.map((g, i) => `${i + 1}. ${g}`), + ``, + `File: ${f.file.split('/').pop()}`, + ].join('\\n'); + + console.log(`[${fileIndex}/${total}] ${f.status} ${f.provider.padEnd(22)} ${f.taskShort}`); + + openFile(f.file); + const action = showDialog(title, message); + + if (action === 'skip') { + console.log('Skipping remaining files.'); + process.exit(0); + } + } +} + +console.log(''); +console.log('Review complete!'); diff --git a/examples/collaboration/ai-node-sdk/client/src/components/chat/suggestion-chips.tsx b/examples/collaboration/ai-node-sdk/client/src/components/chat/suggestion-chips.tsx index cf0e385cfa..3360ad2218 100644 --- a/examples/collaboration/ai-node-sdk/client/src/components/chat/suggestion-chips.tsx +++ b/examples/collaboration/ai-node-sdk/client/src/components/chat/suggestion-chips.tsx @@ -25,6 +25,10 @@ const SUGGESTIONS = [ label: 'Find dates', prompt: 'Find all mentions of dates and highlight them', }, + { + label: 'Executive summary', + prompt: 'Create an executive summary at the beginning of the document', + }, ]; interface SuggestionChipsProps { From 4c04ebd11f30fec4ddf8e98b291d2e1e1b307f18 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:43:39 -0300 Subject: [PATCH 16/26] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20minProperties,=20RichContentInsertInput=20type,=20deduplicat?= =?UTF-8?q?e=20alignment=20constant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/document-api/src/contract/schemas.ts | 37 ++++++++++--------- packages/document-api/src/insert/insert.ts | 30 +++++++++++++-- .../plan-engine/executor.ts | 11 +----- .../plan-engine/paragraphs-wrappers.ts | 2 +- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 5b4c8d7155..e684595c23 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -4869,24 +4869,27 @@ const operationSchemas: Record = { id: { type: 'string' }, op: { const: 'format.apply', type: 'string' }, where: stepWhereSchema, - args: objectSchema( - { - inline: buildInlineRunPatchSchema(), - alignment: { - type: 'string', - enum: ['left', 'center', 'right', 'justify'], - description: - 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', - }, - scope: { - type: 'string', - enum: ['match', 'block'], - description: - 'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".', + args: { + ...objectSchema( + { + inline: buildInlineRunPatchSchema(), + alignment: { + type: 'string', + enum: ['left', 'center', 'right', 'justify'], + description: + 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', + }, + scope: { + type: 'string', + enum: ['match', 'block'], + description: + 'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".', + }, }, - }, - [], // No individual field is required — at least one must be present - ), + [], // No individual field is required + ), + minProperties: 1, // At least one of inline, alignment, or scope must be present + }, }, ['id', 'op', 'where', 'args'], ); diff --git a/packages/document-api/src/insert/insert.ts b/packages/document-api/src/insert/insert.ts index 9dec1a62ed..5ddd31cde4 100644 --- a/packages/document-api/src/insert/insert.ts +++ b/packages/document-api/src/insert/insert.ts @@ -3,6 +3,8 @@ import type { SelectionTarget, TargetLocator, SDMutationReceipt } from '../types import type { SDInsertInput } from '../types/structural-input.js'; import type { SDFragment } from '../types/fragment.js'; import type { StoryLocator } from '../types/story.types.js'; +import type { BlockNodeAddress } from '../types/base.js'; +import type { Placement } from '../types/placement.js'; import { PLACEMENT_VALUES } from '../types/placement.js'; import { DocumentApiValidationError } from '../errors.js'; import { @@ -40,6 +42,25 @@ export type TextInsertInput = OptionalInsertLocator & { in?: StoryLocator; }; +/** + * Extended input for markdown/html inserts that also accept BlockNodeAddress + * targets and placement. These route through the structural insert path. + */ +export type RichContentInsertInput = { + /** Block target for positioned inserts. Accepts BlockNodeAddress or SelectionTarget. */ + target?: SelectionTarget | BlockNodeAddress; + /** Optional mutation ref. Mutually exclusive with target. */ + ref?: string; + /** The markdown/html content to insert. */ + value: string; + /** Content format — must be 'markdown' or 'html' for this input shape. */ + type: 'markdown' | 'html'; + /** Where to place content relative to target. Only valid with BlockNodeAddress targets. */ + placement?: Placement; + /** Target a specific document story. */ + in?: StoryLocator; +}; + /** @deprecated Use {@link TextInsertInput} instead. */ export type LegacyInsertInput = TextInsertInput; @@ -53,7 +74,7 @@ export type LegacyInsertInput = TextInsertInput; * Discrimination: presence of `content` (structural) vs `value` (text string). * These are mutually exclusive — providing both is an error. */ -export type InsertInput = TextInsertInput | SDInsertInput; +export type InsertInput = TextInsertInput | RichContentInsertInput | SDInsertInput; // --------------------------------------------------------------------------- // Allowlists for strict field validation @@ -276,7 +297,6 @@ export function executeInsert( } // Text string path - const { target, ref, value } = input; const contentType = input.type ?? 'text'; // For non-text content types, delegate to the adapter's structured insert path. @@ -284,8 +304,12 @@ export function executeInsert( return writeAdapter.insertStructured(input, normalizeMutationOptions(options)); } + // After the non-text branch, input is guaranteed to be a plain TextInsertInput. + const textInput = input as TextInsertInput; + const { target, ref, value } = textInput; + // Text path with target/ref → route through SelectionMutationAdapter - const storyIn = input.in; + const storyIn = textInput.in; if (target || ref) { const request = target ? { kind: 'insert' as const, target, text: value, ...(storyIn ? { in: storyIn } : {}) } diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts index 9fa7901eca..aa6c2de7a0 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts @@ -39,6 +39,7 @@ import type { } from './executor-registry.types.js'; import { getStepExecutor } from './executor-registry.js'; import { planError } from './errors.js'; +import { ALIGNMENT_TO_JUSTIFICATION } from './paragraphs-wrappers.js'; import { closeHistory } from 'prosemirror-history'; import { yUndoPluginKey } from 'y-prosemirror'; import { checkRevision, getRevision } from './revision-tracker.js'; @@ -804,21 +805,13 @@ export function executeTextDelete( return { changed: true }; } -/** Alignment API value → OOXML justification value */ -const ALIGNMENT_TO_JUSTIFICATION: Record = { - left: 'left', - center: 'center', - right: 'right', - justify: 'both', -}; - /** * Applies alignment to the paragraph node(s) that contain the given range. * Uses the same mechanism as paragraphsSetAlignmentWrapper: updates * paragraphProperties.justification via tr.setNodeMarkup. */ function applyAlignmentToRange(tr: Transaction, absFrom: number, absTo: number, alignment: string): boolean { - const justification = ALIGNMENT_TO_JUSTIFICATION[alignment]; + const justification = ALIGNMENT_TO_JUSTIFICATION[alignment as keyof typeof ALIGNMENT_TO_JUSTIFICATION]; if (!justification) return false; let changed = false; diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts index c50ea3c09a..5f26e674e2 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts @@ -238,7 +238,7 @@ function mutateParagraphProperties( // Alignment mapping — external API → OOXML justification value // --------------------------------------------------------------------------- -const ALIGNMENT_TO_JUSTIFICATION: Record = { +export const ALIGNMENT_TO_JUSTIFICATION: Record = { left: 'left', center: 'center', right: 'right', From ee17a463c6442cc88ca12369b9d3e402bdb269c8 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:48:11 -0300 Subject: [PATCH 17/26] =?UTF-8?q?Revert=20"fix:=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20minProperties,=20RichContentInsertInput=20type,=20d?= =?UTF-8?q?eduplicate=20alignment=20constant"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4c04ebd11f30fec4ddf8e98b291d2e1e1b307f18. --- packages/document-api/src/contract/schemas.ts | 37 +++++++++---------- packages/document-api/src/insert/insert.ts | 30 ++------------- .../plan-engine/executor.ts | 11 +++++- .../plan-engine/paragraphs-wrappers.ts | 2 +- 4 files changed, 30 insertions(+), 50 deletions(-) diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index e684595c23..5b4c8d7155 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -4869,27 +4869,24 @@ const operationSchemas: Record = { id: { type: 'string' }, op: { const: 'format.apply', type: 'string' }, where: stepWhereSchema, - args: { - ...objectSchema( - { - inline: buildInlineRunPatchSchema(), - alignment: { - type: 'string', - enum: ['left', 'center', 'right', 'justify'], - description: - 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', - }, - scope: { - type: 'string', - enum: ['match', 'block'], - description: - 'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".', - }, + args: objectSchema( + { + inline: buildInlineRunPatchSchema(), + alignment: { + type: 'string', + enum: ['left', 'center', 'right', 'justify'], + description: + 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', }, - [], // No individual field is required - ), - minProperties: 1, // At least one of inline, alignment, or scope must be present - }, + scope: { + type: 'string', + enum: ['match', 'block'], + description: + 'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".', + }, + }, + [], // No individual field is required — at least one must be present + ), }, ['id', 'op', 'where', 'args'], ); diff --git a/packages/document-api/src/insert/insert.ts b/packages/document-api/src/insert/insert.ts index 5ddd31cde4..9dec1a62ed 100644 --- a/packages/document-api/src/insert/insert.ts +++ b/packages/document-api/src/insert/insert.ts @@ -3,8 +3,6 @@ import type { SelectionTarget, TargetLocator, SDMutationReceipt } from '../types import type { SDInsertInput } from '../types/structural-input.js'; import type { SDFragment } from '../types/fragment.js'; import type { StoryLocator } from '../types/story.types.js'; -import type { BlockNodeAddress } from '../types/base.js'; -import type { Placement } from '../types/placement.js'; import { PLACEMENT_VALUES } from '../types/placement.js'; import { DocumentApiValidationError } from '../errors.js'; import { @@ -42,25 +40,6 @@ export type TextInsertInput = OptionalInsertLocator & { in?: StoryLocator; }; -/** - * Extended input for markdown/html inserts that also accept BlockNodeAddress - * targets and placement. These route through the structural insert path. - */ -export type RichContentInsertInput = { - /** Block target for positioned inserts. Accepts BlockNodeAddress or SelectionTarget. */ - target?: SelectionTarget | BlockNodeAddress; - /** Optional mutation ref. Mutually exclusive with target. */ - ref?: string; - /** The markdown/html content to insert. */ - value: string; - /** Content format — must be 'markdown' or 'html' for this input shape. */ - type: 'markdown' | 'html'; - /** Where to place content relative to target. Only valid with BlockNodeAddress targets. */ - placement?: Placement; - /** Target a specific document story. */ - in?: StoryLocator; -}; - /** @deprecated Use {@link TextInsertInput} instead. */ export type LegacyInsertInput = TextInsertInput; @@ -74,7 +53,7 @@ export type LegacyInsertInput = TextInsertInput; * Discrimination: presence of `content` (structural) vs `value` (text string). * These are mutually exclusive — providing both is an error. */ -export type InsertInput = TextInsertInput | RichContentInsertInput | SDInsertInput; +export type InsertInput = TextInsertInput | SDInsertInput; // --------------------------------------------------------------------------- // Allowlists for strict field validation @@ -297,6 +276,7 @@ export function executeInsert( } // Text string path + const { target, ref, value } = input; const contentType = input.type ?? 'text'; // For non-text content types, delegate to the adapter's structured insert path. @@ -304,12 +284,8 @@ export function executeInsert( return writeAdapter.insertStructured(input, normalizeMutationOptions(options)); } - // After the non-text branch, input is guaranteed to be a plain TextInsertInput. - const textInput = input as TextInsertInput; - const { target, ref, value } = textInput; - // Text path with target/ref → route through SelectionMutationAdapter - const storyIn = textInput.in; + const storyIn = input.in; if (target || ref) { const request = target ? { kind: 'insert' as const, target, text: value, ...(storyIn ? { in: storyIn } : {}) } diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts index aa6c2de7a0..9fa7901eca 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts @@ -39,7 +39,6 @@ import type { } from './executor-registry.types.js'; import { getStepExecutor } from './executor-registry.js'; import { planError } from './errors.js'; -import { ALIGNMENT_TO_JUSTIFICATION } from './paragraphs-wrappers.js'; import { closeHistory } from 'prosemirror-history'; import { yUndoPluginKey } from 'y-prosemirror'; import { checkRevision, getRevision } from './revision-tracker.js'; @@ -805,13 +804,21 @@ export function executeTextDelete( return { changed: true }; } +/** Alignment API value → OOXML justification value */ +const ALIGNMENT_TO_JUSTIFICATION: Record = { + left: 'left', + center: 'center', + right: 'right', + justify: 'both', +}; + /** * Applies alignment to the paragraph node(s) that contain the given range. * Uses the same mechanism as paragraphsSetAlignmentWrapper: updates * paragraphProperties.justification via tr.setNodeMarkup. */ function applyAlignmentToRange(tr: Transaction, absFrom: number, absTo: number, alignment: string): boolean { - const justification = ALIGNMENT_TO_JUSTIFICATION[alignment as keyof typeof ALIGNMENT_TO_JUSTIFICATION]; + const justification = ALIGNMENT_TO_JUSTIFICATION[alignment]; if (!justification) return false; let changed = false; diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts index 5f26e674e2..c50ea3c09a 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts @@ -238,7 +238,7 @@ function mutateParagraphProperties( // Alignment mapping — external API → OOXML justification value // --------------------------------------------------------------------------- -export const ALIGNMENT_TO_JUSTIFICATION: Record = { +const ALIGNMENT_TO_JUSTIFICATION: Record = { left: 'left', center: 'center', right: 'right', From e95f430d30a0eefa25a2f380a80f6bfd719703af Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:53:34 -0300 Subject: [PATCH 18/26] fix(document-api): add minProperties, type export, shared alignment constant --- packages/document-api/src/contract/schemas.ts | 37 ++++++++++--------- packages/document-api/src/insert/insert.ts | 17 ++++++++- .../plan-engine/executor.ts | 11 ++---- .../plan-engine/paragraphs-wrappers.ts | 2 +- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 5b4c8d7155..4222915a24 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -4869,24 +4869,27 @@ const operationSchemas: Record = { id: { type: 'string' }, op: { const: 'format.apply', type: 'string' }, where: stepWhereSchema, - args: objectSchema( - { - inline: buildInlineRunPatchSchema(), - alignment: { - type: 'string', - enum: ['left', 'center', 'right', 'justify'], - description: - 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', - }, - scope: { - type: 'string', - enum: ['match', 'block'], - description: - 'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".', + args: { + ...objectSchema( + { + inline: buildInlineRunPatchSchema(), + alignment: { + type: 'string', + enum: ['left', 'center', 'right', 'justify'], + description: + 'Set paragraph alignment on the target block(s). Can be combined with inline formatting in the same step.', + }, + scope: { + type: 'string', + enum: ['match', 'block'], + description: + 'When "block", inline formatting expands to cover the entire parent paragraph(s), not just the matched text. Use "block" after markdown inserts to format whole paragraphs with a short identifying pattern. Default: "match".', + }, }, - }, - [], // No individual field is required — at least one must be present - ), + [], + ), + minProperties: 1, + }, }, ['id', 'op', 'where', 'args'], ); diff --git a/packages/document-api/src/insert/insert.ts b/packages/document-api/src/insert/insert.ts index 9dec1a62ed..86a98410aa 100644 --- a/packages/document-api/src/insert/insert.ts +++ b/packages/document-api/src/insert/insert.ts @@ -3,7 +3,8 @@ import type { SelectionTarget, TargetLocator, SDMutationReceipt } from '../types import type { SDInsertInput } from '../types/structural-input.js'; import type { SDFragment } from '../types/fragment.js'; import type { StoryLocator } from '../types/story.types.js'; -import { PLACEMENT_VALUES } from '../types/placement.js'; +import { type BlockNodeAddress } from '../types/base.js'; +import { PLACEMENT_VALUES, type Placement } from '../types/placement.js'; import { DocumentApiValidationError } from '../errors.js'; import { isRecord, @@ -40,6 +41,20 @@ export type TextInsertInput = OptionalInsertLocator & { in?: StoryLocator; }; +/** + * Type-safe input for markdown/html inserts with block-level positioning. + * Accepts BlockNodeAddress targets and placement (routed through the structural insert path). + * Standalone export — not part of the InsertInput union to avoid type narrowing issues in the runtime. + */ +export type RichContentInsertInput = { + target?: SelectionTarget | BlockNodeAddress; + ref?: string; + value: string; + type: 'markdown' | 'html'; + placement?: Placement; + in?: StoryLocator; +}; + /** @deprecated Use {@link TextInsertInput} instead. */ export type LegacyInsertInput = TextInsertInput; diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts index 9fa7901eca..86839931bd 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts @@ -39,6 +39,7 @@ import type { } from './executor-registry.types.js'; import { getStepExecutor } from './executor-registry.js'; import { planError } from './errors.js'; +import { ALIGNMENT_TO_JUSTIFICATION } from './paragraphs-wrappers.js'; import { closeHistory } from 'prosemirror-history'; import { yUndoPluginKey } from 'y-prosemirror'; import { checkRevision, getRevision } from './revision-tracker.js'; @@ -804,13 +805,7 @@ export function executeTextDelete( return { changed: true }; } -/** Alignment API value → OOXML justification value */ -const ALIGNMENT_TO_JUSTIFICATION: Record = { - left: 'left', - center: 'center', - right: 'right', - justify: 'both', -}; +// ALIGNMENT_TO_JUSTIFICATION imported from paragraphs-wrappers.js /** * Applies alignment to the paragraph node(s) that contain the given range. @@ -818,7 +813,7 @@ const ALIGNMENT_TO_JUSTIFICATION: Record = { * paragraphProperties.justification via tr.setNodeMarkup. */ function applyAlignmentToRange(tr: Transaction, absFrom: number, absTo: number, alignment: string): boolean { - const justification = ALIGNMENT_TO_JUSTIFICATION[alignment]; + const justification = ALIGNMENT_TO_JUSTIFICATION[alignment as keyof typeof ALIGNMENT_TO_JUSTIFICATION]; if (!justification) return false; let changed = false; diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts index c50ea3c09a..5f26e674e2 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/paragraphs-wrappers.ts @@ -238,7 +238,7 @@ function mutateParagraphProperties( // Alignment mapping — external API → OOXML justification value // --------------------------------------------------------------------------- -const ALIGNMENT_TO_JUSTIFICATION: Record = { +export const ALIGNMENT_TO_JUSTIFICATION: Record = { left: 'left', center: 'center', right: 'right', From 16d6a523186b74c031fbf429cd19bc4ab6834b14 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:55:09 -0300 Subject: [PATCH 19/26] docs(sdk): require fontSize on headings after markdown insert --- packages/document-api/src/contract/operation-definitions.ts | 1 + packages/sdk/tools/prompt-templates/system-prompt-core.md | 6 +++++- .../sdk/tools/prompt-templates/system-prompt-mcp-header.md | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 2ba5b01101..6d8a6e102c 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -151,6 +151,7 @@ export const INTENT_GROUP_META: Record = { 'IMPORTANT: After a markdown insert, follow up with ONE superdoc_mutations call using format.apply steps to match the document style. ' + 'Each format.apply step accepts "inline" (fontFamily, fontSize, bold, underline, color) AND "alignment" ("left","center","right","justify") in the same step — combine both in one step per block. ' + 'Look at nearby headings and paragraphs in the get_content response and copy their exact formatting. Do NOT invent values — match what is already in the document. ' + + 'ALWAYS include fontSize on headings — markdown headings inherit a large default size that must be overridden to match the document. Use scope: "block" so formatting covers the entire paragraph. ' + 'Also supports replace, delete, and undo/redo. For replace and delete, pass a "ref" from superdoc_search or superdoc_get_content blocks. ' + 'A search ref covers only the matched substring; a block ref covers the entire block text, so use block refs when rewriting or shortening whole paragraphs. ' + 'Refs expire after any mutation; always re-search before the next edit. ' + diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index e049c08398..164bb89ca6 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -158,7 +158,11 @@ superdoc_mutations({action: "apply", atomic: true, steps: [ ]}) ``` -CRITICAL: Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. Use `scope: "block"` so the formatting covers the ENTIRE paragraph, not just the matched text. +CRITICAL rules for formatting after insert: +- Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. +- Use `scope: "block"` so the formatting covers the ENTIRE paragraph, not just the matched text. +- ALWAYS include `fontSize` on headings. Markdown headings inherit a large default font size from the Heading1 style. You MUST override it with the fontSize used by existing titles/headings in the document (typically the same as body text, e.g., 10pt or 12pt). +- If the document blocks don't show a fontSize, use the fontSize from the nearest block that does have one. Total: 3 calls (read + insert + format-all-in-one-batch). Never more. diff --git a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md index f635a68653..3736709cc6 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md @@ -37,7 +37,7 @@ superdoc_mutations({action: "apply", atomic: true, steps: [ {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify", scope: "block"}} ]}) ``` -One format.apply step per block. Combine `inline`, `alignment`, and `scope: "block"` in each step. The pattern only needs to identify which paragraph — `scope: "block"` formats the entire paragraph, not just the matched text. +One format.apply step per block. Combine `inline`, `alignment`, and `scope: "block"` in each step. The pattern only needs to identify which paragraph. ALWAYS include `fontSize` on headings — markdown headings inherit a large default size that must be overridden to match the document. **When to use which tool:** - Creating headings, paragraphs, or any block content → `superdoc_edit` with type "markdown" (preferred, even for a single heading + paragraph) From 8da40b0620509d0765410d55a02b0a1e1e19452f Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:56:21 -0300 Subject: [PATCH 20/26] docs(sdk): context-driven formatting guidance for markdown inserts --- .../src/contract/operation-definitions.ts | 8 ++--- .../prompt-templates/system-prompt-core.md | 29 +++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 6d8a6e102c..a7dd763fe4 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -148,10 +148,10 @@ export const INTENT_GROUP_META: Record = { 'ALWAYS use action "insert" with type "markdown" to create headings, paragraphs, or any block content — this is faster and creates proper document structure in one call. Do NOT use superdoc_create for headings or paragraphs. ' + 'The markdown parser creates headings from # markers (# = Heading1, ## = Heading2), bold from **text**, italic from *text*, and numbered/bullet lists. ' + 'Position markdown inserts with "target" (a BlockNodeAddress like {kind:"block", nodeType, nodeId}) and "placement" (before, after, insideStart, insideEnd). Without a target, content appends at the end of the document. ' + - 'IMPORTANT: After a markdown insert, follow up with ONE superdoc_mutations call using format.apply steps to match the document style. ' + - 'Each format.apply step accepts "inline" (fontFamily, fontSize, bold, underline, color) AND "alignment" ("left","center","right","justify") in the same step — combine both in one step per block. ' + - 'Look at nearby headings and paragraphs in the get_content response and copy their exact formatting. Do NOT invent values — match what is already in the document. ' + - 'ALWAYS include fontSize on headings — markdown headings inherit a large default size that must be overridden to match the document. Use scope: "block" so formatting covers the entire paragraph. ' + + 'IMPORTANT: After a markdown insert, analyze the document context (what kind of document, how titles and body text are styled) and follow up with ONE superdoc_mutations call to format inserted blocks so they look like they belong. ' + + 'Each format.apply step accepts "inline" (fontFamily, fontSize, bold, underline, color), "alignment", and "scope" in the same step. ' + + 'Use scope: "block" so formatting covers the entire paragraph. ALWAYS include fontSize — especially on headings, which inherit a large default size that must be overridden. ' + + 'Copy exact formatting from nearby blocks in the get_content response. Do NOT invent values. Your inserted content must be visually indistinguishable from the rest of the document. ' + 'Also supports replace, delete, and undo/redo. For replace and delete, pass a "ref" from superdoc_search or superdoc_get_content blocks. ' + 'A search ref covers only the matched substring; a block ref covers the entire block text, so use block refs when rewriting or shortening whole paragraphs. ' + 'Refs expire after any mutation; always re-search before the next edit. ' + diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index 164bb89ca6..934090997e 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -130,12 +130,15 @@ Use preset "disc" for bullets, "decimal" for numbered. WARNING: the range conver ### Insert content into a document (new or existing) -Markdown insert creates block structure but uses default formatting. You MUST follow up with formatting to match the document's existing style. +Markdown insert creates block structure but uses default formatting. You MUST follow up with formatting so inserted content looks like it belongs in the document. -**Step 1: Read existing formatting** from the initial get_content blocks response. Pay special attention to: -- **Nearby headings/titles**: look at their fontFamily, fontSize, bold, underline, alignment (especially center vs justify vs left). Your new headings must match these exactly. -- **Body paragraphs**: note fontFamily, fontSize, color, alignment. Your new paragraphs must match. -- Match the style of the nearest similar element, not arbitrary values. +**Step 1: Understand the document context** from the get_content blocks response. Before inserting anything, analyze: +- What kind of document is this? (contract, letter, certificate, report, etc.) +- How are titles/headings styled? (centered? left? bold? underlined? what fontSize?) +- How is body text styled? (fontFamily, fontSize, alignment, color) +- What formatting conventions does the document follow? + +Your inserted content must be indistinguishable from the existing content. If titles are centered 10pt Times New Roman with no bold, your heading must be centered 10pt Times New Roman with no bold. If body text is justified 12pt with underline, your paragraphs must be justified 12pt with underline. **Step 2: Insert content with markdown:** @@ -146,24 +149,20 @@ superdoc_edit({action: "insert", type: "markdown", value: "# Executive Summary\n\nThis agreement sets forth the principal terms..."}) ``` -**Step 3: Apply ALL formatting in a SINGLE superdoc_mutations call.** Each format.apply step accepts `inline` (text styles), `alignment` (paragraph alignment), and `scope` — combine them all in one step per block. +**Step 3: Format ALL inserted blocks in ONE superdoc_mutations call.** Each format.apply step accepts `inline`, `alignment`, and `scope: "block"`. + +Use `scope: "block"` so the formatting covers the entire paragraph (not just the matched text). The text pattern only needs to identify which block. -ALWAYS use `scope: "block"` after markdown inserts. This makes the formatting cover the entire paragraph, not just the matched text pattern. The pattern only needs to uniquely identify which paragraph — a short prefix is enough. +ALWAYS include `fontSize` on every block, especially headings. Markdown headings inherit a large default font size that must be overridden. If a block in get_content doesn't show fontSize, use the fontSize from the nearest block that does. -Example: if the document has centered, underlined, 12pt headings and justified 12pt body text: +Example: document has centered, non-bold, 12pt titles and justified 12pt body text: ``` superdoc_mutations({action: "apply", atomic: true, steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "center", scope: "block"}}, {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}} ]}) ``` -CRITICAL rules for formatting after insert: -- Do NOT guess formatting values. Copy them from the existing document blocks you read in step 1. -- Use `scope: "block"` so the formatting covers the ENTIRE paragraph, not just the matched text. -- ALWAYS include `fontSize` on headings. Markdown headings inherit a large default font size from the Heading1 style. You MUST override it with the fontSize used by existing titles/headings in the document (typically the same as body text, e.g., 10pt or 12pt). -- If the document blocks don't show a fontSize, use the fontSize from the nearest block that does have one. - Total: 3 calls (read + insert + format-all-in-one-batch). Never more. ### Batch multiple text edits atomically From e61a515f0a98de3c69526819c5dd3dd731d813d2 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 19:59:45 -0300 Subject: [PATCH 21/26] docs(sdk): only set properties explicitly present in document blocks --- .../src/contract/operation-definitions.ts | 4 ++-- .../tools/prompt-templates/system-prompt-core.md | 14 +++++++++++--- .../prompt-templates/system-prompt-mcp-header.md | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index a7dd763fe4..037c0c8de2 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -150,8 +150,8 @@ export const INTENT_GROUP_META: Record = { 'Position markdown inserts with "target" (a BlockNodeAddress like {kind:"block", nodeType, nodeId}) and "placement" (before, after, insideStart, insideEnd). Without a target, content appends at the end of the document. ' + 'IMPORTANT: After a markdown insert, analyze the document context (what kind of document, how titles and body text are styled) and follow up with ONE superdoc_mutations call to format inserted blocks so they look like they belong. ' + 'Each format.apply step accepts "inline" (fontFamily, fontSize, bold, underline, color), "alignment", and "scope" in the same step. ' + - 'Use scope: "block" so formatting covers the entire paragraph. ALWAYS include fontSize — especially on headings, which inherit a large default size that must be overridden. ' + - 'Copy exact formatting from nearby blocks in the get_content response. Do NOT invent values. Your inserted content must be visually indistinguishable from the rest of the document. ' + + 'Use scope: "block" so formatting covers the entire paragraph. ' + + 'ONLY set inline properties that are explicitly shown in the existing get_content blocks. If blocks do not show fontSize, do NOT set fontSize (it inherits the document default). Do NOT invent values — mirror exactly what the document blocks show. ' + 'Also supports replace, delete, and undo/redo. For replace and delete, pass a "ref" from superdoc_search or superdoc_get_content blocks. ' + 'A search ref covers only the matched substring; a block ref covers the entire block text, so use block refs when rewriting or shortening whole paragraphs. ' + 'Refs expire after any mutation; always re-search before the next edit. ' + diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index 934090997e..0c8bccb296 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -153,12 +153,20 @@ superdoc_edit({action: "insert", type: "markdown", Use `scope: "block"` so the formatting covers the entire paragraph (not just the matched text). The text pattern only needs to identify which block. -ALWAYS include `fontSize` on every block, especially headings. Markdown headings inherit a large default font size that must be overridden. If a block in get_content doesn't show fontSize, use the fontSize from the nearest block that does. +ONLY include inline properties that are explicitly present in the existing blocks. If the document blocks don't show `fontSize`, do NOT set fontSize (it will inherit the document's default, which is correct). If they don't show `bold`, don't set bold. Mirror exactly what you see, nothing more. Inventing values (like assuming fontSize: 12) causes mismatches. -Example: document has centered, non-bold, 12pt titles and justified 12pt body text: +Example: document blocks show fontFamily and color but NO fontSize, titles are centered: ``` superdoc_mutations({action: "apply", atomic: true, steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "center", scope: "block"}}, + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", color: "#000000"}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", color: "#000000"}, scope: "block"}} +]}) +``` + +Example: document blocks explicitly show fontSize: 12, titles are centered and underlined: +``` +superdoc_mutations({action: "apply", atomic: true, steps: [ + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}} ]}) ``` diff --git a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md index 3736709cc6..440d859466 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-mcp-header.md @@ -37,7 +37,7 @@ superdoc_mutations({action: "apply", atomic: true, steps: [ {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12}, alignment: "justify", scope: "block"}} ]}) ``` -One format.apply step per block. Combine `inline`, `alignment`, and `scope: "block"` in each step. The pattern only needs to identify which paragraph. ALWAYS include `fontSize` on headings — markdown headings inherit a large default size that must be overridden to match the document. +One format.apply step per block. Combine `inline`, `alignment`, and `scope: "block"` in each step. ONLY set properties that are explicitly shown in the existing document blocks. If blocks don't show fontSize, don't set it (the document default will apply correctly). Do NOT invent values. **When to use which tool:** - Creating headings, paragraphs, or any block content → `superdoc_edit` with type "markdown" (preferred, even for a single heading + paragraph) From f39f1279f84fd70045eb05758d8c078f19ff8428 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 20:06:38 -0300 Subject: [PATCH 22/26] feat(super-editor): resolve default fontSize in get_content blocks response --- .../src/contract/operation-definitions.ts | 2 +- .../prompt-templates/system-prompt-core.md | 18 +++------- .../plan-engine/blocks-wrappers.ts | 36 +++++++++++++++++-- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 037c0c8de2..dae4e6b989 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -151,7 +151,7 @@ export const INTENT_GROUP_META: Record = { 'IMPORTANT: After a markdown insert, analyze the document context (what kind of document, how titles and body text are styled) and follow up with ONE superdoc_mutations call to format inserted blocks so they look like they belong. ' + 'Each format.apply step accepts "inline" (fontFamily, fontSize, bold, underline, color), "alignment", and "scope" in the same step. ' + 'Use scope: "block" so formatting covers the entire paragraph. ' + - 'ONLY set inline properties that are explicitly shown in the existing get_content blocks. If blocks do not show fontSize, do NOT set fontSize (it inherits the document default). Do NOT invent values — mirror exactly what the document blocks show. ' + + 'Copy the exact property values from the existing get_content blocks (fontFamily, fontSize, color, alignment, bold, underline). Do NOT invent values — use what the blocks show. ' + 'Also supports replace, delete, and undo/redo. For replace and delete, pass a "ref" from superdoc_search or superdoc_get_content blocks. ' + 'A search ref covers only the matched substring; a block ref covers the entire block text, so use block refs when rewriting or shortening whole paragraphs. ' + 'Refs expire after any mutation; always re-search before the next edit. ' + diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index 0c8bccb296..9dedc051e1 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -151,23 +151,13 @@ superdoc_edit({action: "insert", type: "markdown", **Step 3: Format ALL inserted blocks in ONE superdoc_mutations call.** Each format.apply step accepts `inline`, `alignment`, and `scope: "block"`. -Use `scope: "block"` so the formatting covers the entire paragraph (not just the matched text). The text pattern only needs to identify which block. +Use `scope: "block"` so formatting covers the entire paragraph (not just the matched text). The text pattern only needs to identify which block. Copy the exact property values from the existing blocks in the get_content response. Do NOT invent values. -ONLY include inline properties that are explicitly present in the existing blocks. If the document blocks don't show `fontSize`, do NOT set fontSize (it will inherit the document's default, which is correct). If they don't show `bold`, don't set bold. Mirror exactly what you see, nothing more. Inventing values (like assuming fontSize: 12) causes mismatches. - -Example: document blocks show fontFamily and color but NO fontSize, titles are centered: -``` -superdoc_mutations({action: "apply", atomic: true, steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", color: "#000000"}, alignment: "center", scope: "block"}}, - {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", color: "#000000"}, scope: "block"}} -]}) -``` - -Example: document blocks explicitly show fontSize: 12, titles are centered and underlined: +Example: document blocks show fontFamily, fontSize: 10, color, titles centered: ``` superdoc_mutations({action: "apply", atomic: true, steps: [ - {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, underline: true}, alignment: "center", scope: "block"}}, - {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 12, color: "#000000"}, alignment: "justify", scope: "block"}} + {id: "f1", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "Executive Summary"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 10, color: "#000000"}, alignment: "center", scope: "block"}}, + {id: "f2", op: "format.apply", where: {by: "select", select: {type: "text", pattern: "This agreement sets forth"}, require: "first"}, args: {inline: {fontFamily: "Times New Roman, serif", fontSize: 10, color: "#000000"}, scope: "block"}} ]}) ``` diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts index 35e8034897..dee69db13b 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts @@ -35,6 +35,7 @@ import { requireEditorCommand, rejectTrackedMode } from '../helpers/mutation-hel import { executeDomainCommand } from './plan-wrappers.js'; import { getRevision } from './revision-tracker.js'; import { encodeV4Ref } from '../story-runtime/story-ref-codec.js'; +import { readTranslatedLinkedStyles } from '../../core/parts/adapters/styles-read.js'; // --------------------------------------------------------------------------- // Constants @@ -66,10 +67,36 @@ function extractTextPreview(node: ProseMirrorNode): string | null { const HEADING_PATTERN = /^Heading(\d)$/; +/** + * Resolve the document's default font size (in points) from the style catalog. + * Falls back through: Normal style rPr → docDefaults rPr → undefined. + * OOXML stores fontSize as half-points (w:sz val), so we divide by 2. + */ +function resolveDefaultFontSizePt(editor: Editor): number | undefined { + const styleProps = readTranslatedLinkedStyles(editor); + if (!styleProps) return undefined; + + // Try Normal style first + const normalStyle = styleProps.styles?.['Normal']; + const normalFontSize = normalStyle?.runProperties?.fontSize; + if (typeof normalFontSize === 'number') return normalFontSize / 2; + + // Fall back to docDefaults + const defaultFontSize = styleProps.docDefaults?.runProperties?.fontSize; + if (typeof defaultFontSize === 'number') return defaultFontSize / 2; + + return undefined; +} + /** * Extract key formatting from a block node's first text run marks. + * When defaultFontSizePt is provided, it's used as fallback when the + * inline marks don't specify a fontSize (common for inherited styles). */ -function extractBlockFormatting(node: ProseMirrorNode): { +function extractBlockFormatting( + node: ProseMirrorNode, + defaultFontSizePt?: number, +): { styleId?: string | null; fontFamily?: string; fontSize?: number; @@ -126,7 +153,7 @@ function extractBlockFormatting(node: ProseMirrorNode): { return { ...(styleId ? { styleId } : {}), ...(fontFamily ? { fontFamily } : {}), - ...(fontSize !== undefined ? { fontSize } : {}), + ...((fontSize ?? defaultFontSizePt) !== undefined ? { fontSize: fontSize ?? defaultFontSizePt } : {}), ...(bold ? { bold } : {}), ...(underline ? { underline } : {}), ...(color ? { color } : {}), @@ -240,6 +267,9 @@ export function blocksListWrapper(editor: Editor, input?: BlocksListInput): Bloc const rev = getRevision(editor); + // Resolve document's default fontSize once for all blocks + const defaultFontSizePt = resolveDefaultFontSizePt(editor); + const blocks: BlockListEntry[] = paged.map((candidate, i) => { const textLength = computeTextContentLength(candidate.node); const ref = @@ -261,7 +291,7 @@ export function blocksListWrapper(editor: Editor, input?: BlocksListInput): Bloc nodeType: candidate.nodeType, textPreview: extractTextPreview(candidate.node), isEmpty: textLength === 0, - ...extractBlockFormatting(candidate.node), + ...extractBlockFormatting(candidate.node, defaultFontSizePt), ...(ref ? { ref } : {}), }; }); From 739a18fe4ee3e05cff43102db123d26aa96438c5 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 20:08:46 -0300 Subject: [PATCH 23/26] fix(super-editor): fallback to 10pt default when styles omit fontSize --- .../plan-engine/blocks-wrappers.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts index dee69db13b..a56f354b56 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts @@ -67,14 +67,17 @@ function extractTextPreview(node: ProseMirrorNode): string | null { const HEADING_PATTERN = /^Heading(\d)$/; +/** OOXML implicit default font size when neither Normal style nor docDefaults specifies one. */ +const OOXML_DEFAULT_FONT_SIZE_PT = 10; + /** * Resolve the document's default font size (in points) from the style catalog. - * Falls back through: Normal style rPr → docDefaults rPr → undefined. + * Falls back through: Normal style rPr → docDefaults rPr → OOXML implicit default (10pt). * OOXML stores fontSize as half-points (w:sz val), so we divide by 2. */ -function resolveDefaultFontSizePt(editor: Editor): number | undefined { +function resolveDefaultFontSizePt(editor: Editor): number { const styleProps = readTranslatedLinkedStyles(editor); - if (!styleProps) return undefined; + if (!styleProps) return OOXML_DEFAULT_FONT_SIZE_PT; // Try Normal style first const normalStyle = styleProps.styles?.['Normal']; @@ -85,7 +88,7 @@ function resolveDefaultFontSizePt(editor: Editor): number | undefined { const defaultFontSize = styleProps.docDefaults?.runProperties?.fontSize; if (typeof defaultFontSize === 'number') return defaultFontSize / 2; - return undefined; + return OOXML_DEFAULT_FONT_SIZE_PT; } /** From 20a134133b185040261d4be4d29a6e91c8dd4c9d Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 20:11:35 -0300 Subject: [PATCH 24/26] fix(super-editor): resolve fontSize per-block via style chain in get_content --- .../plan-engine/blocks-wrappers.ts | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts index a56f354b56..e5eee9a4be 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.ts @@ -67,38 +67,68 @@ function extractTextPreview(node: ProseMirrorNode): string | null { const HEADING_PATTERN = /^Heading(\d)$/; -/** OOXML implicit default font size when neither Normal style nor docDefaults specifies one. */ +/** OOXML implicit default font size when neither styles nor docDefaults specifies one. */ const OOXML_DEFAULT_FONT_SIZE_PT = 10; +/** Pre-built style context for fontSize resolution across all blocks in a list call. */ +interface StyleContext { + styles: Record; + docDefaultsFontSizeHp: number | undefined; +} + +function buildStyleContext(editor: Editor): StyleContext | null { + const styleProps = readTranslatedLinkedStyles(editor); + if (!styleProps) return null; + return { + styles: styleProps.styles ?? {}, + docDefaultsFontSizeHp: + typeof styleProps.docDefaults?.runProperties?.fontSize === 'number' + ? styleProps.docDefaults.runProperties.fontSize + : undefined, + }; +} + /** - * Resolve the document's default font size (in points) from the style catalog. - * Falls back through: Normal style rPr → docDefaults rPr → OOXML implicit default (10pt). + * Resolve the effective font size (in points) for a block by walking its style chain. + * Cascade: inline mark → block's paragraph style → basedOn chain → Normal → docDefaults → 10pt. * OOXML stores fontSize as half-points (w:sz val), so we divide by 2. */ -function resolveDefaultFontSizePt(editor: Editor): number { - const styleProps = readTranslatedLinkedStyles(editor); - if (!styleProps) return OOXML_DEFAULT_FONT_SIZE_PT; +function resolveBlockFontSizePt(styleCtx: StyleContext | null, styleId: string | null | undefined): number { + if (!styleCtx) return OOXML_DEFAULT_FONT_SIZE_PT; + + // Walk the style's basedOn chain (limit depth to avoid infinite loops) + let currentId = styleId ?? 'Normal'; + const visited = new Set(); + while (currentId && !visited.has(currentId)) { + visited.add(currentId); + const style = styleCtx.styles[currentId]; + if (!style) break; + const fs = style.runProperties?.fontSize; + if (typeof fs === 'number') return fs / 2; + currentId = style.basedOn ?? ''; + } - // Try Normal style first - const normalStyle = styleProps.styles?.['Normal']; - const normalFontSize = normalStyle?.runProperties?.fontSize; - if (typeof normalFontSize === 'number') return normalFontSize / 2; + // Try Normal if we haven't already + if (!visited.has('Normal')) { + const normal = styleCtx.styles['Normal']; + const fs = normal?.runProperties?.fontSize; + if (typeof fs === 'number') return fs / 2; + } - // Fall back to docDefaults - const defaultFontSize = styleProps.docDefaults?.runProperties?.fontSize; - if (typeof defaultFontSize === 'number') return defaultFontSize / 2; + // docDefaults + if (typeof styleCtx.docDefaultsFontSizeHp === 'number') return styleCtx.docDefaultsFontSizeHp / 2; return OOXML_DEFAULT_FONT_SIZE_PT; } /** * Extract key formatting from a block node's first text run marks. - * When defaultFontSizePt is provided, it's used as fallback when the - * inline marks don't specify a fontSize (common for inherited styles). + * When styleCtx is provided, resolves fontSize from the block's paragraph style + * chain when inline marks don't specify one (common for inherited styles). */ function extractBlockFormatting( node: ProseMirrorNode, - defaultFontSizePt?: number, + styleCtx?: StyleContext | null, ): { styleId?: string | null; fontFamily?: string; @@ -156,7 +186,11 @@ function extractBlockFormatting( return { ...(styleId ? { styleId } : {}), ...(fontFamily ? { fontFamily } : {}), - ...((fontSize ?? defaultFontSizePt) !== undefined ? { fontSize: fontSize ?? defaultFontSizePt } : {}), + ...(fontSize !== undefined + ? { fontSize } + : styleCtx + ? { fontSize: resolveBlockFontSizePt(styleCtx, styleId) } + : {}), ...(bold ? { bold } : {}), ...(underline ? { underline } : {}), ...(color ? { color } : {}), @@ -270,8 +304,8 @@ export function blocksListWrapper(editor: Editor, input?: BlocksListInput): Bloc const rev = getRevision(editor); - // Resolve document's default fontSize once for all blocks - const defaultFontSizePt = resolveDefaultFontSizePt(editor); + // Build style context once — used to resolve fontSize per-block via style chain + const styleCtx = buildStyleContext(editor); const blocks: BlockListEntry[] = paged.map((candidate, i) => { const textLength = computeTextContentLength(candidate.node); @@ -294,7 +328,7 @@ export function blocksListWrapper(editor: Editor, input?: BlocksListInput): Bloc nodeType: candidate.nodeType, textPreview: extractTextPreview(candidate.node), isEmpty: textLength === 0, - ...extractBlockFormatting(candidate.node, defaultFontSizePt), + ...extractBlockFormatting(candidate.node, styleCtx), ...(ref ? { ref } : {}), }; }); From e0b7e232471bc784e5e68d45cf6ba1305325bf51 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 20:15:49 -0300 Subject: [PATCH 25/26] test(super-editor): add fontSize style chain resolution tests for blocks.list --- .../plan-engine/blocks-wrappers.test.ts | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.test.ts index 95061fa2c0..d0d781adaf 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.test.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/blocks-wrappers.test.ts @@ -635,6 +635,182 @@ describe('blocksListWrapper', () => { expect(block.styleId).toBe('Heading1'); expect(block.headingLevel).toBe(1); }); + + it('resolves fontSize from Normal style when inline marks have no fontSize', () => { + const textNode = createNode('text', [], { + text: 'No inline fontSize', + marks: [{ type: { name: 'textStyle' }, attrs: { fontFamily: 'Times New Roman' } }], + }); + const paragraph = createNode('paragraph', [textNode], { + attrs: { paraId: 'p1', sdBlockId: 'p1' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + // Mock editor with converter that has translatedLinkedStyles + const editor = { + state: { doc }, + converter: { + translatedLinkedStyles: { + docDefaults: { runProperties: { fontSize: 24 } }, + latentStyles: {}, + styles: { + Normal: { runProperties: { fontSize: 20 } }, + }, + }, + }, + } as unknown as Editor; + + const result = blocksListWrapper(editor); + const block = result.blocks[0] as any; + // Normal style has fontSize 20 half-points = 10pt + expect(block.fontSize).toBe(10); + }); + + it('resolves fontSize from basedOn chain when block has a styleId', () => { + const textNode = createNode('text', [], { + text: 'Heading text', + marks: [{ type: { name: 'textStyle' }, attrs: { fontFamily: 'Times New Roman' } }], + }); + const paragraph = createNode('paragraph', [textNode], { + attrs: { paraId: 'p1', sdBlockId: 'p1', paragraphProperties: { styleId: 'Heading1' } }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + const editor = { + state: { doc }, + converter: { + translatedLinkedStyles: { + docDefaults: { runProperties: { fontSize: 20 } }, + latentStyles: {}, + styles: { + Normal: { runProperties: { fontSize: 20 } }, + Heading1: { basedOn: 'Normal', runProperties: { fontSize: 28 } }, + }, + }, + }, + } as unknown as Editor; + + const result = blocksListWrapper(editor); + const block = result.blocks[0] as any; + // Heading1 has fontSize 28 half-points = 14pt + expect(block.fontSize).toBe(14); + }); + + it('walks basedOn chain when style has no fontSize', () => { + const textNode = createNode('text', [], { + text: 'List text', + marks: [{ type: { name: 'textStyle' }, attrs: { fontFamily: 'Arial' } }], + }); + const paragraph = createNode('paragraph', [textNode], { + attrs: { paraId: 'p1', sdBlockId: 'p1', paragraphProperties: { styleId: 'ListParagraph' } }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + const editor = { + state: { doc }, + converter: { + translatedLinkedStyles: { + docDefaults: {}, + latentStyles: {}, + styles: { + Normal: { runProperties: { fontSize: 22 } }, + ListParagraph: { basedOn: 'Normal', runProperties: {} }, + }, + }, + }, + } as unknown as Editor; + + const result = blocksListWrapper(editor); + const block = result.blocks[0] as any; + // ListParagraph has no fontSize, basedOn Normal which has 22 hp = 11pt + expect(block.fontSize).toBe(11); + }); + + it('falls back to docDefaults when no style defines fontSize', () => { + const textNode = createNode('text', [], { + text: 'Default text', + marks: [{ type: { name: 'textStyle' }, attrs: { fontFamily: 'Calibri' } }], + }); + const paragraph = createNode('paragraph', [textNode], { + attrs: { paraId: 'p1', sdBlockId: 'p1' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + const editor = { + state: { doc }, + converter: { + translatedLinkedStyles: { + docDefaults: { runProperties: { fontSize: 24 } }, + latentStyles: {}, + styles: { Normal: { runProperties: {} } }, + }, + }, + } as unknown as Editor; + + const result = blocksListWrapper(editor); + const block = result.blocks[0] as any; + // docDefaults has fontSize 24 hp = 12pt + expect(block.fontSize).toBe(12); + }); + + it('falls back to 10pt OOXML default when nothing defines fontSize', () => { + const textNode = createNode('text', [], { + text: 'Bare text', + marks: [{ type: { name: 'textStyle' }, attrs: { fontFamily: 'Courier' } }], + }); + const paragraph = createNode('paragraph', [textNode], { + attrs: { paraId: 'p1', sdBlockId: 'p1' }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + const editor = { + state: { doc }, + converter: { + translatedLinkedStyles: { + docDefaults: {}, + latentStyles: {}, + styles: {}, + }, + }, + } as unknown as Editor; + + const result = blocksListWrapper(editor); + const block = result.blocks[0] as any; + expect(block.fontSize).toBe(10); + }); + + it('inline fontSize takes precedence over style chain', () => { + const textNode = createNode('text', [], { + text: 'Explicit size', + marks: [{ type: { name: 'textStyle' }, attrs: { fontFamily: 'Arial', fontSize: 16 } }], + }); + const paragraph = createNode('paragraph', [textNode], { + attrs: { paraId: 'p1', sdBlockId: 'p1', paragraphProperties: { styleId: 'Heading1' } }, + isBlock: true, + inlineContent: true, + }); + const doc = createNode('doc', [paragraph], { isBlock: false }); + const editor = { + state: { doc }, + converter: { + translatedLinkedStyles: { + docDefaults: { runProperties: { fontSize: 20 } }, + latentStyles: {}, + styles: { Heading1: { runProperties: { fontSize: 28 } } }, + }, + }, + } as unknown as Editor; + + const result = blocksListWrapper(editor); + const block = result.blocks[0] as any; + // Inline fontSize 16 takes precedence over Heading1's 28hp (14pt) + expect(block.fontSize).toBe(16); + }); }); // --------------------------------------------------------------------------- From c5a322fce27de4342b0c753001ca7dd2d4b9ea65 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 6 Apr 2026 20:17:34 -0300 Subject: [PATCH 26/26] docs(sdk): guide agents to match uppercase title conventions --- packages/sdk/tools/prompt-templates/system-prompt-core.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk/tools/prompt-templates/system-prompt-core.md b/packages/sdk/tools/prompt-templates/system-prompt-core.md index 9dedc051e1..b9abf54373 100644 --- a/packages/sdk/tools/prompt-templates/system-prompt-core.md +++ b/packages/sdk/tools/prompt-templates/system-prompt-core.md @@ -135,10 +135,11 @@ Markdown insert creates block structure but uses default formatting. You MUST fo **Step 1: Understand the document context** from the get_content blocks response. Before inserting anything, analyze: - What kind of document is this? (contract, letter, certificate, report, etc.) - How are titles/headings styled? (centered? left? bold? underlined? what fontSize?) +- Are titles UPPERCASE? (e.g., "EMPLOYMENT AGREEMENT", "RECITALS" → your heading must also be UPPERCASE) - How is body text styled? (fontFamily, fontSize, alignment, color) - What formatting conventions does the document follow? -Your inserted content must be indistinguishable from the existing content. If titles are centered 10pt Times New Roman with no bold, your heading must be centered 10pt Times New Roman with no bold. If body text is justified 12pt with underline, your paragraphs must be justified 12pt with underline. +Your inserted content must be indistinguishable from the existing content. If titles are ALL CAPS centered 10pt, your heading text must also be ALL CAPS centered 10pt. If body text is justified 12pt, your paragraphs must be justified 12pt. **Step 2: Insert content with markdown:**