@@ -45,6 +45,41 @@ type DocsSuggestion =
4545 | DocsStyleChangeSuggestion
4646 | DocsParagraphStyleChangeSuggestion ;
4747
48+ // ---------------------------------------------------------------------------
49+ // Safe field mask constants for docs.documents.get with includeTabsContent.
50+ //
51+ // Using a wildcard like "tabs" or "tabs.documentTab.body" causes the Docs API
52+ // to include suggestion/comment sub-fields (suggestedInsertionIds, etc.) in
53+ // the field mask. The API then rejects the request with:
54+ // "Field mask cannot retrieve comment-specific fields when include_comments
55+ // is false."
56+ // even when suggestionsViewMode is PREVIEW_WITHOUT_SUGGESTIONS, because the
57+ // field mask is validated *before* the view-mode filter is applied.
58+ //
59+ // The fix: enumerate only the fields _readStructuralElement actually reads.
60+ // ---------------------------------------------------------------------------
61+ const _ELEM =
62+ 'textRun(content),' +
63+ 'person(personProperties(name,email)),' +
64+ 'richLink(richLinkProperties(title,uri)),' +
65+ 'dateElement(dateElementProperties(displayText,timestamp))' ;
66+
67+ const _PARA = `paragraph(elements(${ _ELEM } ))` ;
68+
69+ // One level of table nesting is enough for real-world docs.
70+ const _BODY_CONTENT = `${ _PARA } ,table(tableRows(tableCells(content(${ _PARA } ))))` ;
71+
72+ // Shared tab sub-fields (tabId/title + body content).
73+ const _TAB_SUBFIELDS = `tabProperties(tabId,title),documentTab(body(content(${ _BODY_CONTENT } )))` ;
74+
75+ // Full read mask for getText / replaceText — includes title and up to 3
76+ // levels of tab nesting (the maximum Google Docs supports).
77+ const DOCS_READ_FIELDS = `title,tabs(${ _TAB_SUBFIELDS } ,childTabs(${ _TAB_SUBFIELDS } ,childTabs(${ _TAB_SUBFIELDS } )))` ;
78+
79+ // Minimal mask for writeText end-index lookup (only needs endIndex).
80+ const _TAB_END_SUBFIELDS = `tabProperties(tabId),documentTab(body(content(endIndex)))` ;
81+ const DOCS_END_INDEX_FIELDS = `tabs(${ _TAB_END_SUBFIELDS } ,childTabs(${ _TAB_END_SUBFIELDS } ))` ;
82+
4883export class DocsService {
4984 /**
5085 * Recursively flattens a tab tree into a single array,
@@ -75,7 +110,7 @@ export class DocsService {
75110 const res = await docs . documents . get ( {
76111 documentId : id ,
77112 suggestionsViewMode : 'SUGGESTIONS_INLINE' ,
78- fields : 'title, body' ,
113+ fields : 'body' ,
79114 } ) ;
80115
81116 const suggestions : DocsSuggestion [ ] = this . _extractSuggestions (
@@ -90,11 +125,7 @@ export class DocsService {
90125 content : [
91126 {
92127 type : 'text' as const ,
93- text : JSON . stringify (
94- { title : res . data . title , suggestions } ,
95- null ,
96- 2 ,
97- ) ,
128+ text : JSON . stringify ( suggestions , null , 2 ) ,
98129 } ,
99130 ] ,
100131 } ;
@@ -307,8 +338,9 @@ export class DocsService {
307338 // Discover the end index by reading the document (required for tabs)
308339 const res = await docs . documents . get ( {
309340 documentId : id ,
310- fields : 'tabs' ,
341+ fields : DOCS_END_INDEX_FIELDS ,
311342 includeTabsContent : true ,
343+ suggestionsViewMode : 'PREVIEW_WITHOUT_SUGGESTIONS' ,
312344 } ) ;
313345
314346 const tabs = this . _flattenTabs ( res . data . tabs || [ ] ) ;
@@ -543,11 +575,11 @@ export class DocsService {
543575 const docs = await this . getDocsClient ( ) ;
544576 const res = await docs . documents . get ( {
545577 documentId : id ,
546- fields : 'title,tabs' , // Request title and tabs (body is legacy and mutually exclusive with tabs in mask)
578+ fields : DOCS_READ_FIELDS ,
547579 includeTabsContent : true ,
580+ suggestionsViewMode : 'PREVIEW_WITHOUT_SUGGESTIONS' ,
548581 } ) ;
549582
550- const docTitle = res . data . title ;
551583 const tabs = this . _flattenTabs ( res . data . tabs || [ ] ) ;
552584
553585 // If tabId is provided, try to find it
@@ -570,9 +602,6 @@ export class DocsService {
570602 }
571603
572604 let text = '' ;
573- if ( docTitle ) {
574- text += `Document Title: ${ docTitle } \n\n` ;
575- }
576605 content . forEach ( ( element ) => {
577606 text += this . _readStructuralElement ( element ) ;
578607 } ) ;
@@ -603,9 +632,6 @@ export class DocsService {
603632 if ( tabs . length === 1 ) {
604633 const tab = tabs [ 0 ] ;
605634 let text = '' ;
606- if ( docTitle ) {
607- text += `Document Title: ${ docTitle } \n\n` ;
608- }
609635 if ( tab . documentTab ?. body ?. content ) {
610636 tab . documentTab . body . content . forEach ( ( element ) => {
611637 text += this . _readStructuralElement ( element ) ;
@@ -641,7 +667,7 @@ export class DocsService {
641667 content : [
642668 {
643669 type : 'text' as const ,
644- text : JSON . stringify ( { title : docTitle , tabs : tabsData } , null , 2 ) ,
670+ text : JSON . stringify ( tabsData , null , 2 ) ,
645671 } ,
646672 ] ,
647673 } ;
@@ -736,8 +762,9 @@ export class DocsService {
736762 // Get the document to find where the text will be replaced
737763 const docBefore = await docs . documents . get ( {
738764 documentId : id ,
739- fields : 'tabs' ,
765+ fields : DOCS_READ_FIELDS ,
740766 includeTabsContent : true ,
767+ suggestionsViewMode : 'PREVIEW_WITHOUT_SUGGESTIONS' ,
741768 } ) ;
742769
743770 const tabs = this . _flattenTabs ( docBefore . data . tabs || [ ] ) ;
0 commit comments