Skip to content

Google Docs sync: push rebuilds as suggestions without losing comments #2

@davmlaw

Description

@davmlaw

Problem

After building a .docx with vibepaper and importing it into Google Docs for collaborative review, every rebuild requires re-uploading the entire document — destroying all comments and track changes history.

Proposed solution

Paragraph-level diffing of rendered markdown, with changes pushed to Google Docs as suggestions (tracked changes). Existing comments on unchanged text survive. The lab head sees exactly what changed and can accept/reject.

This fits vibepaper especially well because most rebuilds change only numbers (updated CSVs), so diffs are small and surgical.

Two phases

Phase 1: vibepaper diff (no external dependencies)

  • Cache fully-rendered markdown (build/.last_render.md) after each build
  • New vibepaper diff command compares previous cache vs current render at paragraph level
  • Shows which paragraphs changed with old → new context
  • Immediately useful for manual updates too

Phase 2: vibepaper sync (Google Docs API)

  • OAuth2 authentication (.vibepaper/credentials.json from Google Cloud Console)
  • First sync: creates a Google Doc from rendered content, stores doc ID
  • Subsequent syncs: diffs against last-synced version, pushes only changed paragraphs as suggestions via Docs API batchUpdate with suggestedInsertionIds/suggestedDeletionIds
  • --dry-run flag for safety

Key design decisions

  • Paragraph as the diff unit — blocks separated by blank lines (headings, prose, tables each treated atomically)
  • Sync state in .vibepaper/ not paper.toml — sync state is user-specific and mutable; paper.toml is version-controlled project config
  • Google API deps are optionalpip install vibepaper[sync]; Phase 1 needs no new deps
  • Fuzzy paragraph matching — uses difflib.SequenceMatcher to match local paragraphs to Google Doc positions, tolerating minor edits in the Doc

Files

Action File
Create src/vibepaper/diff.py — paragraph parsing, diffing, caching
Create src/vibepaper/gdocs.py — Google Docs API: auth, create, sync suggestions
Create tests/test_diff.py, tests/test_gdocs.py
Modify src/vibepaper/build.py — hook save_cache() after table rendering
Modify src/vibepaper/cli.py — add diff and sync subcommands
Modify pyproject.toml — add [project.optional-dependencies] sync

Known limitations

  • Tables inserted as plain text (not native Docs tables)
  • Markdown bold/italic not converted to Docs formatting
  • Images/figures not synced
  • Heavy collaborative editing in the Doc may degrade paragraph matching — --dry-run helps
Detailed implementation plan with code snippets

See the full plan at: .claude/plans/sorted-brewing-gosling.md (local)

Phase 1: diff module

Core abstraction — a Paragraph dataclass (kind, text, line_start) with a fingerprint() method for normalized comparison. parse_paragraphs() splits rendered markdown on blank lines, classifying blocks as heading/prose/table. diff_paragraphs() uses difflib.SequenceMatcher on fingerprints to produce ParagraphChange objects (added/removed/changed with heading context).

Cache hook in build.py:run_build() at line 319 (after table-rendering loop, before pandoc):

rendered_text = concatenate_sections(build_paper, all_sections)
save_cache(rendered_text, build_dir)

vibepaper diff re-runs Jinja2 + tables (no pandoc) and diffs against the cached previous build.

Phase 2: Google Docs sync

Auth: OAuth2 installed-app flow via google-auth-oauthlib. Client secret at .vibepaper/credentials.json, refresh token cached at .vibepaper/token.json.

Initial sync: create_doc() inserts full text via insertText, then applies heading styles via updateParagraphStyle.

Subsequent syncs:

  1. get_doc_paragraphs() reads the Doc's structural paragraph elements with start/end indices
  2. match_paragraphs() fuzzy-matches local changed paragraphs to Doc positions (>0.4 similarity threshold)
  3. build_suggestion_requests() builds batchUpdate requests in reverse index order:
    • Changed: deleteContentRange + insertText with shared suggestedInsertionIds/suggestedDeletionIds
    • Added: insertText with suggestedInsertionIds
    • Removed: deleteContentRange with suggestedDeletionIds
  4. sync_to_doc() executes the batch

Sync state stored in .vibepaper/sync_state.json (doc_id, doc_url) + .vibepaper/last_synced.md (full text for diffing).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions