Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Google Docs Export (publish Markdown to Google Docs)

Take a Markdown string from anywhere in Omniscio and publish it as a fully-formatted new Google Doc: real headings, tables, lists and code, with the URL handed back as a toast and old copies of the same title tidied away.

What it is

Omniscio's Google Docs Export takes a Markdown string from anywhere in the app and publishes it as a fully-formatted new Google Doc — headings, bold/italic, fenced code, lists, blockquotes, links, and native Google Docs tables are all preserved. The destination Doc lives in your Drive (a default folder, or one you configure), the URL comes back as a toast with Open Doc, and older versions of the same title in the same folder are auto-trashed so the destination folder stays clean.

When you trigger a publish from the UI, a persistent "Publishing to Google Docs…" toast with a spinner appears the instant you click, and its text advances through the real publish stages as they happen — Preparing → Creating the document → Adding your content → Formatting → Finishing up — before being replaced by the Open Doc success toast (or a humanized error). It's honest live progress streamed from the main process, not a faked timer, so for the ~3–5 seconds a publish takes you always see that it started and roughly where it is.

The feature is off by default and gated by googleDocsExportEnabled in Settings → Connections → Google Workspace → Docs Export. Turning it on does NOT add an OAuth consent step — Google Docs Export piggybacks on the same single "Connect Google" sign-in that Calendar, Drive, Sheets, and Gmail share, because the publisher uses the documents + drive scopes you already approved at connect time.

Both entry points feed the same publisher pipeline, so a published doc looks the same regardless of where you triggered it from.

Where to find it

How to use it

There are two convergent entry points. Each lands in the same publishMarkdownToGoogleDocs() orchestrator, so the resulting Doc is byte-identical regardless of which surface you used:

  1. Right-click in the UI on six surfaces:

    • A project doc card in the Project Docs section of a project dashboard (ProjectDocsSection.tsx).
    • A scratchpad row in the Scratchpads view (ScratchpadsView.tsx).
    • An agent message bubble in a session (MessageBubble.tsx).
    • A whole session transcript — the Export to Google Docs item in the session ⋯ → Exports submenu (SessionOverflowMenu.tsx; handler handlePublishToGoogleDocs in SessionPanel.tsx). Publishes the same visible-only transcript the other session exports use. Shown only when this feature is enabled.
    • A markdown file link inside chat — right-clicking a .md or .markdown link in any agent or user message (agent-markdown-helpers.tsx → CopyableLink). The menu item appears next to the existing Copy path + Share as public artifact entries, but only when the link's extension is .md or .markdown. The file is read off disk in the main process; the document title defaults to the file's basename (minus the extension).
    • The File Peek Viewer — when you click a file link to open the side-panel previewer (FilePeekOverlay.tsx), markdown files get a Export to Google Docs button in the action bar next to Copy contents / Share / Edit. Hidden for non-markdown files.

    Every surface shows Export to Google Docs in its right-click menu / overflow / action bar. Title defaults to the doc/scratchpad name, the message's first heading, or — for path-mode surfaces — the file's basename. A success toast surfaces the URL with Open Doc for one-click navigation. All five routes share usePublishMarkdownToGoogleDoc.ts, which centralises the feature-flag gate, empty-content guard, error→toast mapping, and the amc://settings/integrations/google-account re-auth deeplink. The hook accepts XOR input — either markdown (content surfaces) or path (file-link surfaces) — and the IPC handler delegates path-mode reads to the main-process read-markdown-from-path.ts helper so the file-read limits stay consistent with the CLI route.

  2. External AI / CLI via POST /gdoc/publish on Omniscio's local control server (127.0.0.1:19519 by default). Body shape is { markdown, title? } OR { path, title? } (a local .md / .markdown file). Bearer-token auth, 1 MB body cap, 2 MB markdown cap, 10/min mutation bucket. Returns the same { docId, url, title, trashedOlderCount } shape as the IPC handler. Useful when you want claude.ai, the phone web UI, or a third-party CLI to publish straight into your Omniscio Drive without re-implementing the conversion pipeline. The CLI route shares the readMarkdownFromPath() helper with the IPC handler, so a Drive document built from a local file is byte-identical whether the file was opened from the chat right-click menu or from an external curl call.

How it behaves

How it works

The end-to-end pipeline is the same three-batch apply for every entry point — porting it once into /src/main/services/google-docs/publisher.ts means a markdown blob renders identically whether the originator was a right-click menu or an external HTTP POST.

Auth. google-auth-service.ts owns the shared Google OAuth2Client and the refresh token. The publisher calls getAuthClient() to mint a googleapis docs.v1 + drive.v3 pair. If the user isn't signed in OR the documents scope wasn't granted, PublisherError throws with GDOC_NOT_AUTHENTICATED / GDOC_SCOPE_INSUFFICIENT and needsReauth: true — every consumer surfaces this as a toast with a Re-authorize action that deep-links to Settings → Connections → Google Workspace.

Markdown → request batches. markdown-to-doc.ts parses Markdown via marked and emits four arrays:

  • insertRequests — the raw text insertions (headings flattened to plain text, bullets prefixed with tabs).
  • listRequests — createParagraphBullets ranges that promote the tab-prefixed lines into native lists (the tab characters are removed by this batch).
  • styleRequests — updateTextStyle (bold/italic/code/links) + updateParagraphStyle (heading levels, blockquotes) requests, whose offsets are then re-adjusted by adjustStyleIndicesForTabRemoval() to compensate for the tabs the list batch consumed.
  • tableDescriptors — placeholder regions that tables.ts replaces with native Docs tables in a follow-up pass.

The three-batch apply order is LOCKED — text → lists → styles + tables — because Google Docs' batchUpdate index math depends on each step having already happened. Re-ordering produces visibly broken output (style runs misaligned by one character per bullet, lists with stray tabs, tables inserted into the wrong paragraphs). The orchestrator owns the order; the parser only provides the batches.

Orchestrator. publishMarkdownToGoogleDocs() runs:

  1. Copy the template doc → new file in the destination folder, renamed to the resolved title (the template's footer with page numbering is preserved on the copy).
  2. documents.get to find the first tabId (templates use tabs to keep the page number footer; if the copy has zero tabs the template is misconfigured → GDOC_NO_TAB).
  3. Clear the template body via a deleteContentRange on the tab (footer survives because it lives outside the body segment).
  4. Three sequential batchUpdate calls — insert, lists, styles — each scoped to the resolved tabId.
  5. applyTables() — replaces table placeholders with native Docs tables.
  6. Trash older versions of the same title in the destination folder (drive.files.list with a name = ... and ... in parents and mimeType = 'application/vnd.google-apps.document' and trashed = false query, then files.update {trashed: true} for each match older than the just-published doc). Non-fatal on failure — publish has already succeeded; trash failures only log.

Live progress. The publisher takes an optional onProgress(phase) and calls it between the steps above (preparing → create → insert → format → finalize) — purely additive, it never reorders the locked three-batch apply. When a UI publish supplies a renderer-minted progressId, the IPC handler bridges onProgress to an emitPush(GDOC_PUBLISH_PROGRESS, { progressId, phase }) push; the shared hook (usePublishMarkdownToGoogleDoc.ts) shows the persistent "Publishing…" toast (a loading spinner), advances its text per phase for its own progressId only (a unique coalesceKey keeps concurrent publishes independent), and removes it on resolve. The CLI route sends no progressId, so it streams nothing. Progress is an enhancement layer — the start + success/error toasts work even if no event arrives. Full invariant: I7 in google-docs-export-contract.md.

Defaults + overrides. Template doc ID and destination folder ID resolve in priority order: explicit input → user setting (googleDocsTemplateId / googleDocsDestFolderId) → hard-coded defaults baked into the publisher. The hard-coded defaults match the originating standalone skill at ~/.claude/skills/google-docs-from-markdown/, so a user with no overrides gets the same output as the skill they may have used before Omniscio shipped the integration.

Stable error codes. Every entry point returns the same PublisherErrorCode discriminator (or wraps it in an IPC envelope / HTTP status code, but the underlying code is the same string).

On the CLI route the HTTP code is a shared ERROR_CODES member instead (F557) — FORBIDDEN, UNPROCESSABLE, NOT_FOUND, SERVICE_UNAVAILABLE, INTERNAL. The strings below stay the publisher's and the IPC envelope's discriminator, and the CLI route keeps them on its server-side log line and telemetry; a CLI client must branch on the shared code.

  • GDOC_FEATURE_DISABLED — googleDocsExportEnabled is off (IPC + CLI gate; the publisher itself does not check the flag).
  • GDOC_NOT_AUTHENTICATED — no refresh token in config OR the token was revoked.
  • GDOC_SCOPE_INSUFFICIENT — the documents scope was never granted at connect time (user must re-auth).
  • GDOC_PARSE_FAILED — markdown-to-doc.ts threw on the input (malformed markdown that the parser can't recover from).
  • GDOC_API_FAILED — docs.batchUpdate / drive.files.copy / etc. returned a non-401 error (network, quota, throttle).
  • GDOC_NO_TAB — the copied template has zero tabs (template misconfigured — user needs to fix the template doc).

IPC handler. google-doc-handlers.ts wraps the publisher in a wrapHandler('publish markdown to google doc', ...) that always returns { success: true, data: GdocPublishMarkdownResult } — the discriminated-union result carries the same error code on failure, so the renderer never sees a raw thrown string. The handler is the gate that enforces googleDocsExportEnabled (the publisher itself is flag-agnostic so the same code path can be reused by the CLI route without re-checking).

Renderer recovery. The right-click surfaces publish through usePublishMarkdownToGoogleDoc.ts, which humanizes every failure before it reaches a toast — no raw invalid_grant / token expired / HTTP-status text is ever shown, via the shared friendlyGoogleProductError translator (the same one Drive / Sheets / Calendar use). An expired or revoked Google session (GDOC_NOT_AUTHENTICATED / GDOC_SCOPE_INSUFFICIENT) shows a Re-authorize toast action that opens Settings → Google Workspace to reconnect; an unenabled Docs API shows an Enable Docs API link. (Navigation goes through useWizardNavigationStore.setPendingSection, not window.location.hash — Omniscio has no hash router.)

CLI route. cli-server.ts § registerGdocRoutes wires POST /gdoc/publish as an apply-immediately mutation behind the standard bearer-token gate + 10/min mutation bucket. Accepts either an inline markdown blob or a path to a local .md / .markdown file (read off disk inside the route). HTTP status mapping: 200 published, 400 bad body, 401 auth missing, 403 feature disabled, 413 body too large, 422 parse failed, 502 Google API failed. Safe to retry: an identical repeat — same X-Client-Request-Id header, or the same publish inputs when no header is sent — returns the doc the first call already created (idempotent: true) instead of making a duplicate, so a dropped response or network retry can't double-publish.

gog CLI fallback (resilience). If a publish is refused because the Docs API isn't enabled on Omniscio's Google Cloud project — or a Docs scope is missing — Omniscio automatically retries the publish through the local gog CLI, on your connected Google account, so the export still succeeds. Both entry points (the IPC handler and the CLI route) publish through publishMarkdownToGoogleDocsWithFallback, so every surface is covered. It's best-effort and safe-by-default: it only activates when gog is installed and signed into the same account Omniscio is connected to (guarded so a doc can never land in the wrong account), it's gated by the googleCliFallbackEnabled setting (on by default), and if gog can't help it falls back closed to the normal humanized error. Wiring this also made the publisher atomic — a failed publish no longer leaves an empty template copy in your Drive. Engineering detail + invariants: google-cli-fallback-contract.md.

Settings UI. GoogleDocsExportSettings.tsx is the collapsible card inside GoogleWorkspaceSettings.tsx — three settings, all reachable via Settings search: google-docs-export-enabled (the toggle), google-docs-template-id (template Doc ID override, optional), google-docs-dest-folder-id (destination folder ID override, optional). The card sits alongside Calendar / Drive / Sheets because all four piggyback on the same Google sign-in.

Integration registry. Entry google-docs-export in integration-registry.ts — kind: 'channel-adapter', featureFlag: 'googleDocsExportEnabled', settingsSearchId: 'google-docs-export-enabled', llmDocPath: 'docs/llm-library/google-docs-export.md'. The CI completeness lint enforces that every claim on the row has real backing wiring.

Telemetry. Every publish attempt fires gdoc_published via trackEvent() — attempted on failure (with errorCode), published on success (with markdownChars and trashedOlderCount). The free-form surface label distinguishes the originator: renderer:project-doc, renderer:scratchpad, renderer:message-bubble, renderer:chat-file-link, renderer:file-peek from the five right-click surfaces, cli from the HTTP route, and ipc / unknown as fallbacks when no label is supplied.

Related

  • INDEX.md — full library index
  • google-integrations.md — umbrella doc for Calendar + Drive + Sheets + Gmail, shared OAuth; documents the reverse route POST /gdoc/import (Google Doc → markdown), a separate feature gated by googleDocImportEnabled
  • drive-integration.md — sibling Google integration using the same OAuth client
  • sheets-integration.md — sibling Google integration with similar agent-driven UX

Last verified 2026-10-02