Skip to content

Edit tables in a directly bound Google Doc - #1069

Draft
Maximo-Guk wants to merge 3 commits into
mainfrom
maximo/docs-tables
Draft

Maximo-Guk wants to merge 3 commits into
mainfrom
maximo/docs-tables

Conversation

@Maximo-Guk

@Maximo-Guk Maximo-Guk commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

WIP -

Lets agents edit tables in a directly bound Google Doc: change the text in cells, insert a table, add and remove rows and columns, and delete a table. On main a table can be read, as HTML in the tab's Markdown, but any edit that touches one is refused.

API

  • replaceText() now works inside a table. A replacement that lies inside one table changes the text, and the bold, italic, strikethrough and links, of the paragraphs, headings and list items its cells hold, written in the HTML getContent() shows. It cannot add or remove paragraphs, cells, rows or columns.
  • Six new methods on a directly bound Doc: insertTable(table, tabId?), which returns the new table's position, insertTableRows, insertTableColumns, deleteTableRows, deleteTableColumns and deleteTable. A table is named by its position among the tab's tables, and its rows and columns by index.
  • A Doc opened from a Drive binding still has no write methods.

Approvals

  • Cell text stays under "Document edits". Adding tables, rows and columns is a new auto-approvable kind, "Adding tables, rows and columns". Deleting a table or its rows or columns always waits for approval (rule 1: it deletes content).
  • A deletion's card says when what it deletes holds a dropdown or another smart chip, which the Markdown does not show.
  • A row, column or table card shows what is added or deleted first, then the text before the table and its first row, which tell which table it is: the Workshop cuts a long card short.

How a queued change stays on its table

  • A queued row, column or delete change remembers how its table read, and applies only while exactly one table of the tab reads so, wherever in the tab that table then is. While another table reads the same, such a change is refused, and the refusal says to make the two differ first.

New cells

  • A row or column added to a table takes the look of the row or column beside it (alignment, font, size, colour, cell shading). Bold, italic, strikethrough and links are cleared, so the Markdown reads as it was simulated. A new table's cells are plain.
  • Pinned header rows keep their count when a row is added beside one.

Refused, never approximated

  • Row and column changes on a table with a merged cell, anything in a table inside a cell, and the text of a cell paragraph that holds an image, a smart chip or a footnote.
  • Row and column changes on a table that holds suggested rows, columns or cells, its own or those of a table inside one of its cells: deleting a suggested row would discard the suggestion.
  • A row added beside a row in which a cell holds nothing but a dropdown or another smart chip, which Google Docs copies into the new row. The status tables Google Docs offers as building blocks are such tables; supporting them is a follow-up.
  • An addition that would take a tab's tables past 5,000 cells, a table wider than 20 columns, and more new text in one call than answers well inside the client's timeout. These numbers are this connection's own.
  • A table added or deleted between two items of one list, and a deletion that would renumber footnotes or a numbered list.

Simulation

  • Every change replays over the tab's Markdown, so a read shows queued changes as they will apply. The engine is pure (docs-tables.ts, docs-table-actions.ts and the converter) and is tested against a model of a Google Docs body built from live probes of the API (__tests__/docs-body-model.ts).

Also changes main's read path

  • documents.get now asks Google for compact JSON (prettyPrint=false). The client refuses an answer over 10 MiB, and Google's default indentation makes a table-heavy document three and a half times larger: 1,000 empty cells are 4.7 MB by default and 1.3 MB compact.

Known gaps (in the README)

  • Bold, italic or strikethrough written into a paragraph whose named style already sets it (italic in a Heading 6) is stored as inherited, so a read shows the tag until the change applies and not after.
  • Text that holds a heading or a list item, written directly in front of an empty paragraph, is written with one more empty paragraph than the read and the card show. This is the paragraph path's behaviour on main; text added below a new table meets it, because insertTable() leaves an empty paragraph there.
  • A plain paragraph written directly in front of an item of a list that shows no marker becomes an item of that list. Also the paragraph path's behaviour on main.

Not in this PR

  • Merging or splitting cells, cell styling, borders and widths.
  • Adding a row beside a cell that holds only a chip (follow-up).
  • Editing a Doc opened from a Drive binding.

Checked against Google, and not

  • Probed live: where a table, a row and a column land and what indexes they take; what a new cell copies from its neighbour; that a new row copies a lone chip and a new column does not; how suggested rows and columns read; pinned header rows; links and their styles; and how long large batches take (4,000 requests in 10 seconds, 19,000 in 40 to 70).
  • Measured: the Docs API honours prettyPrint=false, and a table cell costs 1.3 to 1.4 KB in compact JSON against 4.7 to 5.1 KB with the default indentation. 5,000 cells are about 7 MiB compact, inside the client's 10 MiB limit; with the default indentation the limit is reached near 2,100 cells.
  • Run end to end on a local instance against a real Google Doc: a cell edit, two row insertions, a column insertion, a table insertion, two row deletions, two column deletions and two table deletions, each approved in the Workshop and read back with the Docs API. Every one applied as its card and the simulated read said, including a deletion whose card showed the table as the change queued before it would leave it.

Testing

  • pnpm --filter @gadgets/google-gatekeeper test:run: 2115 Node, 597 worker and 487 docs-worker cases pass.
  • tsc run directly for both configs, and pnpm lint.
  • The live check above.

Screenshots

Text replaced in a cell.

Approval card: Edit Google Doc, a replacement inside a table

A row added, with its text.

Approval card: Insert table rows in Google Doc

A table added.

Approval card: Insert a table in Google Doc

A row deleted. Deletions offer no "Always approve".

Approval card: Delete table rows in Google Doc

A column deleted.

Approval card: Delete table columns in Google Doc

A table deleted, shown as the column deletion queued before it leaves it.

Approval card: Delete a table in Google Doc

🤖 Generated with Claude Code

@github-actions github-actions Bot added the gatekeeper Changes to a gatekeeper integration label Nov 9, 2026
Comment on lines +2000 to +2003
if (first.link === undefined) {
requests.push({ style: [start, end, textStyle, UNLINKED_FIELDS] });
} else {
requests.push({ style: [start, end, textStyle, FOUR_FIELDS] });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve non-link styling when clearing an existing link. Google documents that removing a link resets the range to the preceding text's style unless those styles are explicitly supplied (TextStyle.link). These requests supply only the three shown styles, so removing a link, or changing the destination of part of one, can also change its font size/family and other unshown formatting. I reproduced this with the PR's applyDocsRequests(..., { unlinkRestyles: true }): removing the anchor around 10pt Docs after a 20pt Prefix changes Docs to 20pt. This contradicts the preservation contract in packages/gatekeeper-google/README.md:377-379 that unlinking changes nothing else of the text's look. Carry the original non-link styling through to the clearing request so a link-only edit preserves it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was checked against Google before the requests were written. In a live probe, clearing a link left the text with its own size, font and bold beside a neighbour that differed, whether the request named four fields or six: only the link, its underline and its colour went. So the reference's "reset to the preceding text's style" is not what the API does today, and the README sentence describes what it does. unlinkRestyles in the test model is the documented behaviour, there so some requests are checked under both; the requests are not written to be safe under it in every case, and yours is one. Restating every style of the run in the clearing request would turn styles the text inherits from its paragraph into styles set on it, so I would rather not do that for a behaviour Google does not show.

@ask-bonk

ask-bonk Bot commented Nov 9, 2026

Copy link
Copy Markdown

Review: 2 findings.

Posted 2 actionable inline comments, verified with targeted reproductions. Additional read-only checks covered 10,000 cell-text replacements and 6,958 structural changes against the PR’s document model.

github run

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Preview: pr1069-maximo-docs-tables

https://pr1069-maximo-docs-tables-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

@ask-bonk

ask-bonk Bot commented Nov 9, 2026

Copy link
Copy Markdown

Since last review: 1 resolved, 1 still open, 0 new.

Both conclusions were confirmed with targeted, read-only reproductions. No additional actionable findings.

github run

Maximo-Guk and others added 3 commits November 9, 2026 13:33
A directly bound Google Doc can now change its tables; on main a table can
be read but any edit that touches one is refused.

`replaceText()` now works inside a table: it changes the text, and the
bold, italic, strikethrough and links, of the paragraphs, headings and list
items a table's cells hold, written in the HTML `getContent()` shows. Six
methods change structure: `insertTable`, `insertTableRows`,
`insertTableColumns`, `deleteTableRows`, `deleteTableColumns` and
`deleteTable`.

Adding tables, rows and columns is a new auto-approvable kind. Deleting a
table or its rows or columns always waits for approval, since it deletes
content.

A queued change to an existing table remembers how its table read and
applies only while exactly one table of the tab reads so. While another
table reads the same, such a change is refused.

Every change replays over the tab's Markdown, so a read shows queued
changes as they will apply. The engine is checked against a model of a
Google Docs body built from live probes of the API.

What cannot be applied exactly is refused: merged cells, a table inside a
cell, a table that holds suggested rows, columns or cells, a row beside a
cell that holds only a smart chip (Google copies the chip into the new
row), and an addition that would take a tab's tables past 5,000 cells.

The document read now asks Google for compact JSON: the client refuses an
answer over 10 MiB, and Google's default indentation makes a table-heavy
document about four times larger.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The Workshop cuts a long approval card short, and the row, column and
table cards led with the text before the table and its first row, so the
rows a deletion removes sat below the cut. Those fields now follow the
one that shows the change.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Deleting a table, or a row or column of it, deletes the tables its cells
hold, and with them any row, column or cell suggested there. The outer
table's map did not record those, so its deletion card said nothing of
them and a deletion queued before such a suggestion appeared still
applied. The map now looks through nested tables.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@ask-bonk

ask-bonk Bot commented Nov 9, 2026

Copy link
Copy Markdown

Since last review: 1 resolved, 0 still open, 0 new.

LGTM!

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gatekeeper Changes to a gatekeeper integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant