@superdoc/sdk 2.22.0-next.15 → 2.22.0-next.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/actions.cjs +99 -77
- package/dist/agent/actions.js +99 -77
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.d.ts +72 -0
- package/dist/generated/contract.cjs +1 -1
- package/dist/generated/contract.js +1 -1
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/package.json +6 -6
- package/tools/catalog.json +4 -0
- package/tools/generation-receipt.json +16 -16
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +4 -0
- package/tools/tools.generic.json +4 -0
- package/tools/tools.openai.json +4 -0
- package/tools/tools.vercel.json +4 -0
package/dist/agent/actions.cjs
CHANGED
|
@@ -165,13 +165,15 @@ function findSnapshotTextByNodeId(snapshot, nodeId) {
|
|
|
165
165
|
}
|
|
166
166
|
function createdBlockTarget(result) {
|
|
167
167
|
const rec = asRecord(result);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
for (const [key, nodeType] of [
|
|
169
|
+
['paragraph', 'paragraph'],
|
|
170
|
+
['heading', 'heading'],
|
|
171
|
+
['item', 'listItem'],
|
|
172
|
+
]) {
|
|
173
|
+
const block = asRecord(rec?.[key]);
|
|
174
|
+
if (typeof block?.nodeId === 'string' && block.nodeId.length > 0) {
|
|
175
|
+
return { nodeId: block.nodeId, nodeType };
|
|
176
|
+
}
|
|
175
177
|
}
|
|
176
178
|
return null;
|
|
177
179
|
}
|
|
@@ -1547,7 +1549,7 @@ async function runInsertListItems(doc, args) {
|
|
|
1547
1549
|
}
|
|
1548
1550
|
}
|
|
1549
1551
|
async function runAddListItems(doc, args, opts) {
|
|
1550
|
-
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1552
|
+
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
|
|
1551
1553
|
try {
|
|
1552
1554
|
const needle = args.anchorText?.trim();
|
|
1553
1555
|
const anchorNodeId = args.anchorNodeId?.trim();
|
|
@@ -1598,25 +1600,47 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1598
1600
|
}
|
|
1599
1601
|
const anchorPath = blockNumbering(anchor)?.path;
|
|
1600
1602
|
const anchorLevel = Array.isArray(anchorPath) ? Math.max(0, anchorPath.length - 1) : 0;
|
|
1603
|
+
const listByNodeId = new Map((pre.lists ?? []).flatMap((list) => list.items.map((item) => [item.nodeId, list.listId])));
|
|
1604
|
+
const anchorListId = listByNodeId.get(anchor.nodeId);
|
|
1605
|
+
// The lists snapshot is paged, so a missing item is unknown, not foreign.
|
|
1606
|
+
const belongsToAnchorList = (row) => {
|
|
1607
|
+
const rowListId = row.nodeId == null ? undefined : listByNodeId.get(row.nodeId);
|
|
1608
|
+
return anchorListId == null || rowListId == null || rowListId === anchorListId;
|
|
1609
|
+
};
|
|
1601
1610
|
// Place new items AFTER the anchor's whole sub-tree — every following item
|
|
1602
1611
|
// whose numbering PATH is nested under the anchor's path (e.g. [12] ⊂ [12,4]).
|
|
1603
1612
|
// Match by path prefix, not merely "deeper level", and SKIP interleaved
|
|
1604
1613
|
// non-numbered paragraphs (list-item continuations) rather than stopping at
|
|
1605
1614
|
// them — real lists wrap sub-items across plain paragraphs. Stop at the first
|
|
1606
1615
|
// numbered item that is NOT a descendant (a sibling/ancestor, or a different
|
|
1607
|
-
// list). Without this, a new same-level item wedges in among the anchor's
|
|
1616
|
+
// unrelated list). Without this, a new same-level item wedges in among the anchor's
|
|
1608
1617
|
// children and steals the ones after it. No descendants → the anchor itself.
|
|
1609
1618
|
let subtreeEnd = anchor;
|
|
1610
1619
|
const anchorPathArr = Array.isArray(anchorPath) ? anchorPath : null;
|
|
1611
1620
|
const anchorIdx = rows.findIndex((r) => r.nodeId === anchor.nodeId);
|
|
1612
1621
|
if (anchorPathArr && anchorIdx >= 0) {
|
|
1613
1622
|
const isDescendant = (p) => Array.isArray(p) && p.length > anchorPathArr.length && anchorPathArr.every((v, k) => p[k] === v);
|
|
1623
|
+
const descendantListIds = new Set();
|
|
1624
|
+
let lastDescendantIdx = anchorIdx;
|
|
1614
1625
|
for (let i = anchorIdx + 1; i < rows.length; i += 1) {
|
|
1615
1626
|
const p = blockNumbering(rows[i])?.path;
|
|
1616
|
-
|
|
1627
|
+
const listId = listByNodeId.get(rows[i].nodeId ?? '');
|
|
1628
|
+
const foreign = !belongsToAnchorList(rows[i]) && listId != null;
|
|
1629
|
+
// A separate numbering definition counts on its own, so its path need
|
|
1630
|
+
// not start with the anchor's path ([1, 2] under [2]); go by depth.
|
|
1631
|
+
const nestedForeign = foreign && Array.isArray(p) && p.length > anchorPathArr.length;
|
|
1632
|
+
if (isDescendant(p) || nestedForeign) {
|
|
1633
|
+
if (foreign && !descendantListIds.has(listId)) {
|
|
1634
|
+
// Nested bullets can use a separate numbering definition. Its first
|
|
1635
|
+
// item must be contiguous; a new list after plain text is unrelated.
|
|
1636
|
+
if (i !== lastDescendantIdx + 1)
|
|
1637
|
+
break;
|
|
1638
|
+
descendantListIds.add(listId);
|
|
1639
|
+
}
|
|
1617
1640
|
subtreeEnd = rows[i]; // a nested child/grandchild of the anchor
|
|
1641
|
+
lastDescendantIdx = i;
|
|
1618
1642
|
}
|
|
1619
|
-
else if (Array.isArray(p)) {
|
|
1643
|
+
else if (Array.isArray(p) || (blockNumbering(rows[i]) && !belongsToAnchorList(rows[i]))) {
|
|
1620
1644
|
break; // a numbered item that is NOT a descendant → sub-tree ended
|
|
1621
1645
|
}
|
|
1622
1646
|
// else: non-numbered paragraph (a continuation) — skip and keep scanning
|
|
@@ -1634,30 +1658,55 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1634
1658
|
if (firstRelative < 0) {
|
|
1635
1659
|
const firstTargetLevel = Math.max(0, anchorLevel + firstRelative);
|
|
1636
1660
|
const startIdx = rows.findIndex((r) => r.nodeId === subtreeEnd.nodeId);
|
|
1661
|
+
const descendantListIds = new Set();
|
|
1662
|
+
let lastDescendantIdx = startIdx;
|
|
1637
1663
|
for (let i = startIdx + 1; i >= 1 && i < rows.length; i += 1) {
|
|
1638
1664
|
const level = levelOfRow(rows[i]);
|
|
1639
1665
|
if (level == null)
|
|
1640
1666
|
continue; // non-numbered continuation paragraph
|
|
1641
1667
|
if (level <= firstTargetLevel)
|
|
1642
1668
|
break;
|
|
1669
|
+
const listId = listByNodeId.get(rows[i].nodeId ?? '');
|
|
1670
|
+
if (!belongsToAnchorList(rows[i]) && listId != null && !descendantListIds.has(listId)) {
|
|
1671
|
+
if (i !== lastDescendantIdx + 1)
|
|
1672
|
+
break;
|
|
1673
|
+
descendantListIds.add(listId);
|
|
1674
|
+
}
|
|
1643
1675
|
subtreeEnd = rows[i];
|
|
1676
|
+
lastDescendantIdx = i;
|
|
1644
1677
|
}
|
|
1645
1678
|
}
|
|
1679
|
+
const anchorKind = blockNumbering(anchor)?.kind ?? null;
|
|
1680
|
+
// Search nearest first within the destination level. A known list may mix
|
|
1681
|
+
// ordered and bullet levels. Keep the numbering-kind fallback only when
|
|
1682
|
+
// the paged snapshot cannot establish that both items share a list.
|
|
1683
|
+
const findSiblingAt = (level) => {
|
|
1684
|
+
const usable = (r) => !!r?.nodeId &&
|
|
1685
|
+
belongsToAnchorList(r) &&
|
|
1686
|
+
levelOfRow(r) === level &&
|
|
1687
|
+
((anchorListId != null && listByNodeId.get(r.nodeId) === anchorListId) ||
|
|
1688
|
+
(blockNumbering(r)?.kind ?? null) === anchorKind);
|
|
1689
|
+
for (let delta = 1; delta <= rows.length; delta += 1) {
|
|
1690
|
+
const before = anchorIdx >= 0 ? rows[anchorIdx - delta] : undefined;
|
|
1691
|
+
if (usable(before))
|
|
1692
|
+
return before;
|
|
1693
|
+
const after = anchorIdx >= 0 ? rows[anchorIdx + delta] : undefined;
|
|
1694
|
+
if (usable(after))
|
|
1695
|
+
return after;
|
|
1696
|
+
}
|
|
1697
|
+
return undefined;
|
|
1698
|
+
};
|
|
1699
|
+
/** The existing item a new one should look like: same level, nearest first. */
|
|
1700
|
+
const formattingSourceForLevel = (targetLevel) => targetLevel === anchorLevel ? anchor : findSiblingAt(targetLevel);
|
|
1646
1701
|
const attachFn = maybeMethod(doc, ['lists', 'attach']);
|
|
1647
|
-
|
|
1648
|
-
|
|
1702
|
+
const insertFn = maybeMethod(doc, ['lists', 'insert']);
|
|
1703
|
+
if (!attachFn || !insertFn) {
|
|
1704
|
+
throw new errors.SuperDocCliError('doc.lists.insert and doc.lists.attach are required on the document handle.', {
|
|
1649
1705
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
1650
1706
|
});
|
|
1651
1707
|
}
|
|
1652
1708
|
const executed = [];
|
|
1653
|
-
let
|
|
1654
|
-
kind: 'after',
|
|
1655
|
-
target: {
|
|
1656
|
-
kind: 'block',
|
|
1657
|
-
nodeType: subtreeEnd.nodeType ?? 'listItem',
|
|
1658
|
-
nodeId: subtreeEnd.nodeId ?? anchor.nodeId,
|
|
1659
|
-
},
|
|
1660
|
-
};
|
|
1709
|
+
let insertionAnchorId = subtreeEnd.nodeId ?? anchor.nodeId;
|
|
1661
1710
|
const createdItems = [];
|
|
1662
1711
|
for (const entry of args.entries) {
|
|
1663
1712
|
// Styled items carry ordered `runs`; a plain item is a single run. The
|
|
@@ -1670,11 +1719,25 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1670
1719
|
// outline level is clamped at 0 (can't go above the root).
|
|
1671
1720
|
const rel = typeof entry?.level === 'number' ? Math.floor(entry.level) : 0;
|
|
1672
1721
|
const targetLevel = Math.max(0, anchorLevel + rel);
|
|
1673
|
-
const
|
|
1722
|
+
const formattingSource = formattingSourceForLevel(targetLevel) ?? anchor;
|
|
1723
|
+
// Placement can follow a child with different direction. Inherit from
|
|
1724
|
+
// the intended list level independently of that insertion point.
|
|
1725
|
+
const created = await insertFn({
|
|
1726
|
+
target: { kind: 'block', nodeType: 'listItem', nodeId: insertionAnchorId },
|
|
1727
|
+
position: 'after',
|
|
1728
|
+
text,
|
|
1729
|
+
...(formattingSource.nodeId !== insertionAnchorId
|
|
1730
|
+
? { inheritFrom: { kind: 'block', nodeType: 'listItem', nodeId: formattingSource.nodeId } }
|
|
1731
|
+
: {}),
|
|
1732
|
+
...(args.changeMode ? { changeMode: args.changeMode } : {}),
|
|
1733
|
+
}, args.changeMode ? { changeMode: args.changeMode } : undefined);
|
|
1674
1734
|
const target = createdBlockTarget(created);
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1735
|
+
if (!target || target.nodeType !== 'listItem') {
|
|
1736
|
+
throw new errors.SuperDocCliError('doc.lists.insert did not return a created list item.', {
|
|
1737
|
+
code: 'ACTION_FAILED',
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
executed.push({ operationId: 'doc.lists.insert', result: created });
|
|
1678
1741
|
// changeMode is a MutationOption — pass it in the second options arg, not
|
|
1679
1742
|
// the input (lists.attach reads options.changeMode; input.changeMode is ignored).
|
|
1680
1743
|
const attachResult = await attachFn({
|
|
@@ -1690,53 +1753,10 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1690
1753
|
rationale: `level ${targetLevel}`,
|
|
1691
1754
|
});
|
|
1692
1755
|
createdItems.push({ nodeId: target.nodeId, targetLevel, text, runs, itemMarks: entry?.marks });
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
// A new item is created next to the ANCHOR, so it inherits the anchor's
|
|
1698
|
-
// direct indentation — correct at the same level, wrong at any other one.
|
|
1699
|
-
// `lists.attach` sets the numbering level (`w:ilvl`) but leaves that direct
|
|
1700
|
-
// `w:ind` alone, so a level-changing add renders with a sub-item's indent
|
|
1701
|
-
// under a top-level number: e.g. an item promoted to level 0 kept
|
|
1702
|
-
// left=1800 while its "1./2./3." siblings sit at 1080. Copy the indentation
|
|
1703
|
-
// from an EXISTING sibling at the item's own level. Best-effort: an
|
|
1704
|
-
// unmatched indent is cosmetic and must not fail an item that landed.
|
|
1705
|
-
const createdIds = new Set(createdItems.map((ci) => ci.nodeId));
|
|
1706
|
-
const anchorKind = blockNumbering(anchor)?.kind ?? null;
|
|
1707
|
-
// Search OUTWARD from the anchor, nearest first, and only accept items
|
|
1708
|
-
// of the anchor's own numbering kind: a document holds several lists,
|
|
1709
|
-
// and the first level-0 item in document order usually belongs to a
|
|
1710
|
-
// different one (here: a bullet list far above the ordered list being
|
|
1711
|
-
// extended), whose indentation is unrelated.
|
|
1712
|
-
const findSiblingAt = (level) => {
|
|
1713
|
-
const usable = (r) => !!r?.nodeId &&
|
|
1714
|
-
!createdIds.has(r.nodeId) &&
|
|
1715
|
-
levelOfRow(r) === level &&
|
|
1716
|
-
(blockNumbering(r)?.kind ?? null) === anchorKind;
|
|
1717
|
-
for (let delta = 1; delta <= rows.length; delta += 1) {
|
|
1718
|
-
const before = anchorIdx >= 0 ? rows[anchorIdx - delta] : undefined;
|
|
1719
|
-
if (usable(before))
|
|
1720
|
-
return before;
|
|
1721
|
-
const after = anchorIdx >= 0 ? rows[anchorIdx + delta] : undefined;
|
|
1722
|
-
if (usable(after))
|
|
1723
|
-
return after;
|
|
1724
|
-
}
|
|
1725
|
-
return undefined;
|
|
1726
|
-
};
|
|
1727
|
-
/** The existing item a new one should look like: same level, nearest first. */
|
|
1728
|
-
const lookSourceFor = (targetLevel) => targetLevel === anchorLevel ? anchor : findSiblingAt(targetLevel);
|
|
1729
|
-
// Most Word documents keep a list's look in a paragraph STYLE, not in direct
|
|
1730
|
-
// properties: "MB Agr 2", "Quick 1.", even Heading3 carry the numbering, the
|
|
1731
|
-
// indentation and the face. A created paragraph does not get the sibling's
|
|
1732
|
-
// style — it gets the anchor's `next` style (Heading3 → BodyText), a default
|
|
1733
|
-
// (ListParagraph, whose contextualSpacing changes the spacing), or the
|
|
1734
|
-
// anchor's own style when the new item belongs to a DIFFERENT level (keeping
|
|
1735
|
-
// "MB Agr 3" on an item whose numbering now says level 2). Reconcile it with
|
|
1736
|
-
// the sibling at the item's own level.
|
|
1737
|
-
//
|
|
1738
|
-
// setStyleRef, not setStyle: setStyle clears direct run formatting, which
|
|
1739
|
-
// would wipe the sampled run look applied below.
|
|
1756
|
+
insertionAnchorId = target.nodeId;
|
|
1757
|
+
}
|
|
1758
|
+
// Insertion may select a style's `next` style; keep the destination list level's
|
|
1759
|
+
// style instead. setStyleRef preserves the inherited direct run formatting.
|
|
1740
1760
|
const styledItems = [];
|
|
1741
1761
|
const styleSkipped = await bestEffort(async () => {
|
|
1742
1762
|
const setStyleRefFn = maybeMethod(doc, ['styles', 'paragraph', 'setStyleRef']);
|
|
@@ -1745,7 +1765,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1745
1765
|
return;
|
|
1746
1766
|
let current = null;
|
|
1747
1767
|
for (const ci of createdItems) {
|
|
1748
|
-
const source =
|
|
1768
|
+
const source = formattingSourceForLevel(ci.targetLevel);
|
|
1749
1769
|
if (!source)
|
|
1750
1770
|
continue;
|
|
1751
1771
|
const wanted = source.styleId ?? null;
|
|
@@ -1763,6 +1783,8 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1763
1783
|
styledItems.push({ nodeId: ci.nodeId, styleId: wanted });
|
|
1764
1784
|
}
|
|
1765
1785
|
});
|
|
1786
|
+
// lists.attach changes numbering level without normalizing direct indentation.
|
|
1787
|
+
// Match the existing destination level when its indentation is available.
|
|
1766
1788
|
const indentedItems = [];
|
|
1767
1789
|
const indentSkipped = await bestEffort(async () => {
|
|
1768
1790
|
const setIndentFn = maybeMethod(doc, ['format', 'paragraph', 'setIndentation']);
|
|
@@ -1770,7 +1792,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1770
1792
|
if (!setIndentFn || !getNodeFn)
|
|
1771
1793
|
return;
|
|
1772
1794
|
for (const ci of createdItems) {
|
|
1773
|
-
const sibling =
|
|
1795
|
+
const sibling = formattingSourceForLevel(ci.targetLevel);
|
|
1774
1796
|
if (!sibling?.nodeId)
|
|
1775
1797
|
continue;
|
|
1776
1798
|
const node = (await getNodeFn({ id: sibling.nodeId }));
|
|
@@ -1853,7 +1875,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1853
1875
|
// The BASE layer must come from the row the item will sit beside, not
|
|
1854
1876
|
// from the anchor: a promoted or demoted item crosses levels, and the
|
|
1855
1877
|
// anchor's font/size/colour/emphasis is then the wrong level's look.
|
|
1856
|
-
// `
|
|
1878
|
+
// `formattingSourceForLevel(targetLevel)` already picks the destination sibling for
|
|
1857
1879
|
// style and indent, so the look follows the same source. Falls back to
|
|
1858
1880
|
// the anchor only when the destination level has no sibling to sample.
|
|
1859
1881
|
const anchorBase = inlineLookFromRow(anchor);
|
|
@@ -1891,7 +1913,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1891
1913
|
// An item the agent styled itself is left alone by layers 1–2: its
|
|
1892
1914
|
// marks describe the whole intent, and sampling would fight them.
|
|
1893
1915
|
const unstyled = ci.itemMarks == null && !ci.runs.some((r) => r.marks);
|
|
1894
|
-
const source = unstyled ?
|
|
1916
|
+
const source = unstyled ? formattingSourceForLevel(ci.targetLevel) : undefined;
|
|
1895
1917
|
const sourceText = source?.nodeId ? fullTextById.get(source.nodeId) : undefined;
|
|
1896
1918
|
const pattern = source?.nodeId && sourceText ? await listItemRunPattern(doc, sourceText, source.nodeId) : null;
|
|
1897
1919
|
// 1. sampled base over the whole item (row fields first, run-level
|
|
@@ -1971,7 +1993,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1971
1993
|
// promotion keeps the plain marker until that read lands.
|
|
1972
1994
|
let markProps = markRunPropsFromMarks({ ...ci.itemMarks, ...ci.runs[0]?.marks });
|
|
1973
1995
|
if (!markProps && getNodeFn) {
|
|
1974
|
-
const source =
|
|
1996
|
+
const source = formattingSourceForLevel(ci.targetLevel);
|
|
1975
1997
|
if (source?.nodeId)
|
|
1976
1998
|
markProps = paragraphMarkRunPropsOf(await getNodeFn({ id: source.nodeId }));
|
|
1977
1999
|
}
|
package/dist/agent/actions.js
CHANGED
|
@@ -162,13 +162,15 @@ function findSnapshotTextByNodeId(snapshot, nodeId) {
|
|
|
162
162
|
}
|
|
163
163
|
function createdBlockTarget(result) {
|
|
164
164
|
const rec = asRecord(result);
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
for (const [key, nodeType] of [
|
|
166
|
+
['paragraph', 'paragraph'],
|
|
167
|
+
['heading', 'heading'],
|
|
168
|
+
['item', 'listItem'],
|
|
169
|
+
]) {
|
|
170
|
+
const block = asRecord(rec?.[key]);
|
|
171
|
+
if (typeof block?.nodeId === 'string' && block.nodeId.length > 0) {
|
|
172
|
+
return { nodeId: block.nodeId, nodeType };
|
|
173
|
+
}
|
|
172
174
|
}
|
|
173
175
|
return null;
|
|
174
176
|
}
|
|
@@ -1544,7 +1546,7 @@ async function runInsertListItems(doc, args) {
|
|
|
1544
1546
|
}
|
|
1545
1547
|
}
|
|
1546
1548
|
async function runAddListItems(doc, args, opts) {
|
|
1547
|
-
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
1549
|
+
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'lists'] });
|
|
1548
1550
|
try {
|
|
1549
1551
|
const needle = args.anchorText?.trim();
|
|
1550
1552
|
const anchorNodeId = args.anchorNodeId?.trim();
|
|
@@ -1595,25 +1597,47 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1595
1597
|
}
|
|
1596
1598
|
const anchorPath = blockNumbering(anchor)?.path;
|
|
1597
1599
|
const anchorLevel = Array.isArray(anchorPath) ? Math.max(0, anchorPath.length - 1) : 0;
|
|
1600
|
+
const listByNodeId = new Map((pre.lists ?? []).flatMap((list) => list.items.map((item) => [item.nodeId, list.listId])));
|
|
1601
|
+
const anchorListId = listByNodeId.get(anchor.nodeId);
|
|
1602
|
+
// The lists snapshot is paged, so a missing item is unknown, not foreign.
|
|
1603
|
+
const belongsToAnchorList = (row) => {
|
|
1604
|
+
const rowListId = row.nodeId == null ? undefined : listByNodeId.get(row.nodeId);
|
|
1605
|
+
return anchorListId == null || rowListId == null || rowListId === anchorListId;
|
|
1606
|
+
};
|
|
1598
1607
|
// Place new items AFTER the anchor's whole sub-tree — every following item
|
|
1599
1608
|
// whose numbering PATH is nested under the anchor's path (e.g. [12] ⊂ [12,4]).
|
|
1600
1609
|
// Match by path prefix, not merely "deeper level", and SKIP interleaved
|
|
1601
1610
|
// non-numbered paragraphs (list-item continuations) rather than stopping at
|
|
1602
1611
|
// them — real lists wrap sub-items across plain paragraphs. Stop at the first
|
|
1603
1612
|
// numbered item that is NOT a descendant (a sibling/ancestor, or a different
|
|
1604
|
-
// list). Without this, a new same-level item wedges in among the anchor's
|
|
1613
|
+
// unrelated list). Without this, a new same-level item wedges in among the anchor's
|
|
1605
1614
|
// children and steals the ones after it. No descendants → the anchor itself.
|
|
1606
1615
|
let subtreeEnd = anchor;
|
|
1607
1616
|
const anchorPathArr = Array.isArray(anchorPath) ? anchorPath : null;
|
|
1608
1617
|
const anchorIdx = rows.findIndex((r) => r.nodeId === anchor.nodeId);
|
|
1609
1618
|
if (anchorPathArr && anchorIdx >= 0) {
|
|
1610
1619
|
const isDescendant = (p) => Array.isArray(p) && p.length > anchorPathArr.length && anchorPathArr.every((v, k) => p[k] === v);
|
|
1620
|
+
const descendantListIds = new Set();
|
|
1621
|
+
let lastDescendantIdx = anchorIdx;
|
|
1611
1622
|
for (let i = anchorIdx + 1; i < rows.length; i += 1) {
|
|
1612
1623
|
const p = blockNumbering(rows[i])?.path;
|
|
1613
|
-
|
|
1624
|
+
const listId = listByNodeId.get(rows[i].nodeId ?? '');
|
|
1625
|
+
const foreign = !belongsToAnchorList(rows[i]) && listId != null;
|
|
1626
|
+
// A separate numbering definition counts on its own, so its path need
|
|
1627
|
+
// not start with the anchor's path ([1, 2] under [2]); go by depth.
|
|
1628
|
+
const nestedForeign = foreign && Array.isArray(p) && p.length > anchorPathArr.length;
|
|
1629
|
+
if (isDescendant(p) || nestedForeign) {
|
|
1630
|
+
if (foreign && !descendantListIds.has(listId)) {
|
|
1631
|
+
// Nested bullets can use a separate numbering definition. Its first
|
|
1632
|
+
// item must be contiguous; a new list after plain text is unrelated.
|
|
1633
|
+
if (i !== lastDescendantIdx + 1)
|
|
1634
|
+
break;
|
|
1635
|
+
descendantListIds.add(listId);
|
|
1636
|
+
}
|
|
1614
1637
|
subtreeEnd = rows[i]; // a nested child/grandchild of the anchor
|
|
1638
|
+
lastDescendantIdx = i;
|
|
1615
1639
|
}
|
|
1616
|
-
else if (Array.isArray(p)) {
|
|
1640
|
+
else if (Array.isArray(p) || (blockNumbering(rows[i]) && !belongsToAnchorList(rows[i]))) {
|
|
1617
1641
|
break; // a numbered item that is NOT a descendant → sub-tree ended
|
|
1618
1642
|
}
|
|
1619
1643
|
// else: non-numbered paragraph (a continuation) — skip and keep scanning
|
|
@@ -1631,30 +1655,55 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1631
1655
|
if (firstRelative < 0) {
|
|
1632
1656
|
const firstTargetLevel = Math.max(0, anchorLevel + firstRelative);
|
|
1633
1657
|
const startIdx = rows.findIndex((r) => r.nodeId === subtreeEnd.nodeId);
|
|
1658
|
+
const descendantListIds = new Set();
|
|
1659
|
+
let lastDescendantIdx = startIdx;
|
|
1634
1660
|
for (let i = startIdx + 1; i >= 1 && i < rows.length; i += 1) {
|
|
1635
1661
|
const level = levelOfRow(rows[i]);
|
|
1636
1662
|
if (level == null)
|
|
1637
1663
|
continue; // non-numbered continuation paragraph
|
|
1638
1664
|
if (level <= firstTargetLevel)
|
|
1639
1665
|
break;
|
|
1666
|
+
const listId = listByNodeId.get(rows[i].nodeId ?? '');
|
|
1667
|
+
if (!belongsToAnchorList(rows[i]) && listId != null && !descendantListIds.has(listId)) {
|
|
1668
|
+
if (i !== lastDescendantIdx + 1)
|
|
1669
|
+
break;
|
|
1670
|
+
descendantListIds.add(listId);
|
|
1671
|
+
}
|
|
1640
1672
|
subtreeEnd = rows[i];
|
|
1673
|
+
lastDescendantIdx = i;
|
|
1641
1674
|
}
|
|
1642
1675
|
}
|
|
1676
|
+
const anchorKind = blockNumbering(anchor)?.kind ?? null;
|
|
1677
|
+
// Search nearest first within the destination level. A known list may mix
|
|
1678
|
+
// ordered and bullet levels. Keep the numbering-kind fallback only when
|
|
1679
|
+
// the paged snapshot cannot establish that both items share a list.
|
|
1680
|
+
const findSiblingAt = (level) => {
|
|
1681
|
+
const usable = (r) => !!r?.nodeId &&
|
|
1682
|
+
belongsToAnchorList(r) &&
|
|
1683
|
+
levelOfRow(r) === level &&
|
|
1684
|
+
((anchorListId != null && listByNodeId.get(r.nodeId) === anchorListId) ||
|
|
1685
|
+
(blockNumbering(r)?.kind ?? null) === anchorKind);
|
|
1686
|
+
for (let delta = 1; delta <= rows.length; delta += 1) {
|
|
1687
|
+
const before = anchorIdx >= 0 ? rows[anchorIdx - delta] : undefined;
|
|
1688
|
+
if (usable(before))
|
|
1689
|
+
return before;
|
|
1690
|
+
const after = anchorIdx >= 0 ? rows[anchorIdx + delta] : undefined;
|
|
1691
|
+
if (usable(after))
|
|
1692
|
+
return after;
|
|
1693
|
+
}
|
|
1694
|
+
return undefined;
|
|
1695
|
+
};
|
|
1696
|
+
/** The existing item a new one should look like: same level, nearest first. */
|
|
1697
|
+
const formattingSourceForLevel = (targetLevel) => targetLevel === anchorLevel ? anchor : findSiblingAt(targetLevel);
|
|
1643
1698
|
const attachFn = maybeMethod(doc, ['lists', 'attach']);
|
|
1644
|
-
|
|
1645
|
-
|
|
1699
|
+
const insertFn = maybeMethod(doc, ['lists', 'insert']);
|
|
1700
|
+
if (!attachFn || !insertFn) {
|
|
1701
|
+
throw new SuperDocCliError('doc.lists.insert and doc.lists.attach are required on the document handle.', {
|
|
1646
1702
|
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
1647
1703
|
});
|
|
1648
1704
|
}
|
|
1649
1705
|
const executed = [];
|
|
1650
|
-
let
|
|
1651
|
-
kind: 'after',
|
|
1652
|
-
target: {
|
|
1653
|
-
kind: 'block',
|
|
1654
|
-
nodeType: subtreeEnd.nodeType ?? 'listItem',
|
|
1655
|
-
nodeId: subtreeEnd.nodeId ?? anchor.nodeId,
|
|
1656
|
-
},
|
|
1657
|
-
};
|
|
1706
|
+
let insertionAnchorId = subtreeEnd.nodeId ?? anchor.nodeId;
|
|
1658
1707
|
const createdItems = [];
|
|
1659
1708
|
for (const entry of args.entries) {
|
|
1660
1709
|
// Styled items carry ordered `runs`; a plain item is a single run. The
|
|
@@ -1667,11 +1716,25 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1667
1716
|
// outline level is clamped at 0 (can't go above the root).
|
|
1668
1717
|
const rel = typeof entry?.level === 'number' ? Math.floor(entry.level) : 0;
|
|
1669
1718
|
const targetLevel = Math.max(0, anchorLevel + rel);
|
|
1670
|
-
const
|
|
1719
|
+
const formattingSource = formattingSourceForLevel(targetLevel) ?? anchor;
|
|
1720
|
+
// Placement can follow a child with different direction. Inherit from
|
|
1721
|
+
// the intended list level independently of that insertion point.
|
|
1722
|
+
const created = await insertFn({
|
|
1723
|
+
target: { kind: 'block', nodeType: 'listItem', nodeId: insertionAnchorId },
|
|
1724
|
+
position: 'after',
|
|
1725
|
+
text,
|
|
1726
|
+
...(formattingSource.nodeId !== insertionAnchorId
|
|
1727
|
+
? { inheritFrom: { kind: 'block', nodeType: 'listItem', nodeId: formattingSource.nodeId } }
|
|
1728
|
+
: {}),
|
|
1729
|
+
...(args.changeMode ? { changeMode: args.changeMode } : {}),
|
|
1730
|
+
}, args.changeMode ? { changeMode: args.changeMode } : undefined);
|
|
1671
1731
|
const target = createdBlockTarget(created);
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1732
|
+
if (!target || target.nodeType !== 'listItem') {
|
|
1733
|
+
throw new SuperDocCliError('doc.lists.insert did not return a created list item.', {
|
|
1734
|
+
code: 'ACTION_FAILED',
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
executed.push({ operationId: 'doc.lists.insert', result: created });
|
|
1675
1738
|
// changeMode is a MutationOption — pass it in the second options arg, not
|
|
1676
1739
|
// the input (lists.attach reads options.changeMode; input.changeMode is ignored).
|
|
1677
1740
|
const attachResult = await attachFn({
|
|
@@ -1687,53 +1750,10 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1687
1750
|
rationale: `level ${targetLevel}`,
|
|
1688
1751
|
});
|
|
1689
1752
|
createdItems.push({ nodeId: target.nodeId, targetLevel, text, runs, itemMarks: entry?.marks });
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
// A new item is created next to the ANCHOR, so it inherits the anchor's
|
|
1695
|
-
// direct indentation — correct at the same level, wrong at any other one.
|
|
1696
|
-
// `lists.attach` sets the numbering level (`w:ilvl`) but leaves that direct
|
|
1697
|
-
// `w:ind` alone, so a level-changing add renders with a sub-item's indent
|
|
1698
|
-
// under a top-level number: e.g. an item promoted to level 0 kept
|
|
1699
|
-
// left=1800 while its "1./2./3." siblings sit at 1080. Copy the indentation
|
|
1700
|
-
// from an EXISTING sibling at the item's own level. Best-effort: an
|
|
1701
|
-
// unmatched indent is cosmetic and must not fail an item that landed.
|
|
1702
|
-
const createdIds = new Set(createdItems.map((ci) => ci.nodeId));
|
|
1703
|
-
const anchorKind = blockNumbering(anchor)?.kind ?? null;
|
|
1704
|
-
// Search OUTWARD from the anchor, nearest first, and only accept items
|
|
1705
|
-
// of the anchor's own numbering kind: a document holds several lists,
|
|
1706
|
-
// and the first level-0 item in document order usually belongs to a
|
|
1707
|
-
// different one (here: a bullet list far above the ordered list being
|
|
1708
|
-
// extended), whose indentation is unrelated.
|
|
1709
|
-
const findSiblingAt = (level) => {
|
|
1710
|
-
const usable = (r) => !!r?.nodeId &&
|
|
1711
|
-
!createdIds.has(r.nodeId) &&
|
|
1712
|
-
levelOfRow(r) === level &&
|
|
1713
|
-
(blockNumbering(r)?.kind ?? null) === anchorKind;
|
|
1714
|
-
for (let delta = 1; delta <= rows.length; delta += 1) {
|
|
1715
|
-
const before = anchorIdx >= 0 ? rows[anchorIdx - delta] : undefined;
|
|
1716
|
-
if (usable(before))
|
|
1717
|
-
return before;
|
|
1718
|
-
const after = anchorIdx >= 0 ? rows[anchorIdx + delta] : undefined;
|
|
1719
|
-
if (usable(after))
|
|
1720
|
-
return after;
|
|
1721
|
-
}
|
|
1722
|
-
return undefined;
|
|
1723
|
-
};
|
|
1724
|
-
/** The existing item a new one should look like: same level, nearest first. */
|
|
1725
|
-
const lookSourceFor = (targetLevel) => targetLevel === anchorLevel ? anchor : findSiblingAt(targetLevel);
|
|
1726
|
-
// Most Word documents keep a list's look in a paragraph STYLE, not in direct
|
|
1727
|
-
// properties: "MB Agr 2", "Quick 1.", even Heading3 carry the numbering, the
|
|
1728
|
-
// indentation and the face. A created paragraph does not get the sibling's
|
|
1729
|
-
// style — it gets the anchor's `next` style (Heading3 → BodyText), a default
|
|
1730
|
-
// (ListParagraph, whose contextualSpacing changes the spacing), or the
|
|
1731
|
-
// anchor's own style when the new item belongs to a DIFFERENT level (keeping
|
|
1732
|
-
// "MB Agr 3" on an item whose numbering now says level 2). Reconcile it with
|
|
1733
|
-
// the sibling at the item's own level.
|
|
1734
|
-
//
|
|
1735
|
-
// setStyleRef, not setStyle: setStyle clears direct run formatting, which
|
|
1736
|
-
// would wipe the sampled run look applied below.
|
|
1753
|
+
insertionAnchorId = target.nodeId;
|
|
1754
|
+
}
|
|
1755
|
+
// Insertion may select a style's `next` style; keep the destination list level's
|
|
1756
|
+
// style instead. setStyleRef preserves the inherited direct run formatting.
|
|
1737
1757
|
const styledItems = [];
|
|
1738
1758
|
const styleSkipped = await bestEffort(async () => {
|
|
1739
1759
|
const setStyleRefFn = maybeMethod(doc, ['styles', 'paragraph', 'setStyleRef']);
|
|
@@ -1742,7 +1762,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1742
1762
|
return;
|
|
1743
1763
|
let current = null;
|
|
1744
1764
|
for (const ci of createdItems) {
|
|
1745
|
-
const source =
|
|
1765
|
+
const source = formattingSourceForLevel(ci.targetLevel);
|
|
1746
1766
|
if (!source)
|
|
1747
1767
|
continue;
|
|
1748
1768
|
const wanted = source.styleId ?? null;
|
|
@@ -1760,6 +1780,8 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1760
1780
|
styledItems.push({ nodeId: ci.nodeId, styleId: wanted });
|
|
1761
1781
|
}
|
|
1762
1782
|
});
|
|
1783
|
+
// lists.attach changes numbering level without normalizing direct indentation.
|
|
1784
|
+
// Match the existing destination level when its indentation is available.
|
|
1763
1785
|
const indentedItems = [];
|
|
1764
1786
|
const indentSkipped = await bestEffort(async () => {
|
|
1765
1787
|
const setIndentFn = maybeMethod(doc, ['format', 'paragraph', 'setIndentation']);
|
|
@@ -1767,7 +1789,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1767
1789
|
if (!setIndentFn || !getNodeFn)
|
|
1768
1790
|
return;
|
|
1769
1791
|
for (const ci of createdItems) {
|
|
1770
|
-
const sibling =
|
|
1792
|
+
const sibling = formattingSourceForLevel(ci.targetLevel);
|
|
1771
1793
|
if (!sibling?.nodeId)
|
|
1772
1794
|
continue;
|
|
1773
1795
|
const node = (await getNodeFn({ id: sibling.nodeId }));
|
|
@@ -1850,7 +1872,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1850
1872
|
// The BASE layer must come from the row the item will sit beside, not
|
|
1851
1873
|
// from the anchor: a promoted or demoted item crosses levels, and the
|
|
1852
1874
|
// anchor's font/size/colour/emphasis is then the wrong level's look.
|
|
1853
|
-
// `
|
|
1875
|
+
// `formattingSourceForLevel(targetLevel)` already picks the destination sibling for
|
|
1854
1876
|
// style and indent, so the look follows the same source. Falls back to
|
|
1855
1877
|
// the anchor only when the destination level has no sibling to sample.
|
|
1856
1878
|
const anchorBase = inlineLookFromRow(anchor);
|
|
@@ -1888,7 +1910,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1888
1910
|
// An item the agent styled itself is left alone by layers 1–2: its
|
|
1889
1911
|
// marks describe the whole intent, and sampling would fight them.
|
|
1890
1912
|
const unstyled = ci.itemMarks == null && !ci.runs.some((r) => r.marks);
|
|
1891
|
-
const source = unstyled ?
|
|
1913
|
+
const source = unstyled ? formattingSourceForLevel(ci.targetLevel) : undefined;
|
|
1892
1914
|
const sourceText = source?.nodeId ? fullTextById.get(source.nodeId) : undefined;
|
|
1893
1915
|
const pattern = source?.nodeId && sourceText ? await listItemRunPattern(doc, sourceText, source.nodeId) : null;
|
|
1894
1916
|
// 1. sampled base over the whole item (row fields first, run-level
|
|
@@ -1968,7 +1990,7 @@ async function runAddListItems(doc, args, opts) {
|
|
|
1968
1990
|
// promotion keeps the plain marker until that read lands.
|
|
1969
1991
|
let markProps = markRunPropsFromMarks({ ...ci.itemMarks, ...ci.runs[0]?.marks });
|
|
1970
1992
|
if (!markProps && getNodeFn) {
|
|
1971
|
-
const source =
|
|
1993
|
+
const source = formattingSourceForLevel(ci.targetLevel);
|
|
1972
1994
|
if (source?.nodeId)
|
|
1973
1995
|
markProps = paragraphMarkRunPropsOf(await getNodeFn({ id: source.nodeId }));
|
|
1974
1996
|
}
|