---
title: Google Integrations (Calendar, Drive, Sheets)
---

# Google Integrations (Calendar, Drive, Sheets)

## What it is

Omniscio connects to Google Calendar, Google Drive, and Google Sheets through a **single shared OAuth** (one "Connect Google" click gives all three). These integrations are _agent-facing_: Claude gets them as tool-use calls inside a conversation loop, so you ask in plain English ("what's on my calendar Thursday", "create a sheet with those numbers", "share the quarterly report folder with sarah@...") and the agent decides which tool to call, Omniscio executes it via the `googleapis` library, and the agent continues its response using the result. Agent responses come back as markdown with linked Drive files, formatted Sheets tables, or concise event summaries. Calendar additionally has a real browsing UI inside Omniscio: the **Calendar** sidebar project opens a full panel with Agenda / Week / Month views, an event editor, an AI chat drawer, and a Today/Tomorrow sidebar list (see "The Calendar panel" below).

## Where to find it

### How to use it

1. **Connect Google once — from the one Google Account card.** Both **Settings → Google Workspace** and **Settings → Email & Summaries** show a single **Google Account** card at the top with one **Connect** (and, once connected, **Disconnect**) button. Gmail, Calendar, Drive, and Sheets all share this one sign-in, so each product just shows a derived **"Ready"** line underneath instead of its own separate Authenticate button. A browser window opens; approve the scopes in one go. Omniscio catches the redirect on a loopback HTTP server, encrypts the refresh token, and stores it. Because every Google surface reads one shared connection store, **connecting (or disconnecting) anywhere updates all of them live** — no stale "still disconnected" panels. Disconnecting revokes the token at Google, clears everything at once, and deletes the local data Omniscio built from Google — the mailbox copy, the Google Meet list, the email samples saved from Gmail, and the people and profile details built from your email (past daily digests and writing-style guides you created are kept). (Invariants: [google-account-connection-contract.md](../../.claude/memory/contracts/google-account-connection-contract.md).)
2. **Enable the integrations you want.** Each integration has its own enable flag (`gmailEnabled`, `calendarEnabled`, `driveEnabled`, etc.) so you can turn on, say, Calendar without using Drive. The one consent covers every integration — but a switched-off integration is left alone: the daily digest reads Gmail only while `gmailEnabled` is on and Calendar only while `calendarEnabled` is on (and a new install leaves both out of the digest until you add them), and the Gmail bug-intake reader makes no Gmail call until a project has bug intake set up.
3. **Ask the agent something.** In any Claude session, just ask: _"check if I'm free Thursday at 3pm"_, _"create a Drive folder called 2026-campaigns and put the assets from #design-review inside"_, _"read the first sheet of Q1 budget and tell me total spend"_. The agent decides which tool to call from the 23 available (Calendar 6, Drive 7, Sheets 10), executes it through the token-loop, and responds.
4. **Read the results.** Drive operations come back with `webViewLink`s that are clickable. Sheets operations render as markdown tables. Calendar operations return event summaries with links to open the event in Google Calendar's web UI.
5. **Separately: Drive via MCP — force-disabled (CASA Tier 2).** This read-only Drive MCP (search/list/read/info/quota over the user's EXISTING Drive) needed the Tier-3 `drive.readonly` scope, so it is now force-disabled and the shared OAuth requests only `drive.file` — see the scope-change note in [drive-integration.md](drive-integration.md). The built-in Drive tools (step 3) degrade to create-only; full Drive over existing files is moving to a bring-your-own `gog` CLI path (follow-up).
6. **Google Doc paste-to-import (off by default).** Settings → **Google** → **Drive** → toggle **Auto-import pasted Google Doc URLs** on. With it on, pasting a `https://docs.google.com/document/d/...` or `/presentation/d/...` URL into the chat composer fetches the doc as markdown (or slides as a PDF) and inserts it; without it on, the URL pastes as plain text. Hold **Shift** while pasting to force plain-text paste even when the toggle is on. The integration uses the same Google OAuth as Drive — if you haven't connected Google yet, a "Connect Google to import this doc" toast appears with a button that launches the unified consent flow.
   - **Inline images are stripped from the imported markdown.** Google's `text/markdown` export inlines every embedded image as a base64 `data:` URI — typically a 50–200 KB payload apiece, usually clustered at the bottom of the document as reference-style definitions (`[image1]: <data:image/png;base64,…>`). A single image-bearing Doc can balloon a 2k-token document to ~200k tokens once pasted, so Omniscio removes those `data:` image references (both reference definitions and inline `![](data:…)` forms) and keeps the document's text and structure. Normal links and external image URLs (`https://…`) survive untouched. This mirrors the equivalent strip on the clipboard HTML-paste path — see [paste-rich-text.md](paste-rich-text.md). It applies only to the Docs-as-markdown path; Slides imported as PDF keep their visuals natively.
7. **Sheets URLs paste as text.** Pasting a `https://docs.google.com/spreadsheets/d/...` URL drops the URL into the textarea unchanged — there is no paste-time fetch for Sheets, so the toggle in step 6 doesn't apply. The first time you paste a Sheets URL, a one-time dismissible hint suggests asking Claude to read or edit the sheet directly in chat (which uses the 10 Sheets tool-use calls described in step 3). Click "Don't show again" to silence it.
8. **Import a Google Doc as markdown over the CLI (off by default).** The same import capability is exposed to external agents / scripts / the phone over Omniscio's local control server as **`POST /gdoc/import`** (`127.0.0.1:19519`) — the exact reverse of [`POST /gdoc/publish`](google-docs-export.md) (markdown → Doc). Send `{ "url": "<a Google Doc URL or a bare doc id>" }` with the bearer token; get back `{ ok: true, data: { title, markdown, url } }` (full markdown inline). Bearer-auth, 10/min cap, gated by the **same `googleDocImportEnabled` toggle** as step 6 (403 `GDOC_IMPORT_FEATURE_DISABLED` when off). Docs only — a Slides/Sheets link returns `GDOC_IMPORT_BAD_URL`; other failures map to clear codes (`GDOC_IMPORT_NOT_CONNECTED`, `GDOC_IMPORT_NO_ACCESS`, `GDOC_IMPORT_NOT_FOUND`, …) with plain messages that never leak the connected email or raw Google errors. It reuses the same converter as paste-import, so the markdown (with base64 images stripped per step 6) is identical whichever way you import. Full route reference: [cli-server-gating.md § /gdoc/import](../../.claude/memory/cli-server-gating.md).

### The Calendar panel

Beyond the agent tools, the **Calendar** virtual project in the sidebar opens a real calendar UI (`src/renderer/src/features/calendar/`):

- **Three views**: Agenda (the default), Week, and Month, switched by tabs in the panel header (`CalendarHeader.tsx`, view state in `calendar-store.ts`). The panel loads the next 90 days of events and the header carries an event count plus **New Event** and **Refresh** buttons.
- **Every subscribed calendar, with show/hide**: the panel, the sidebar Today/Tomorrow list, and the daily digest show events from ALL your Google calendars at once, each colored by its calendar (not just the primary one). **Settings → Calendar → "Calendars to show"** lists every calendar with a checkbox — uncheck the noisy ones (holidays, birthdays, shared team calendars) to hide them (hiding a calendar also stops its "starting soon" reminders — the reminder scan runs over the same visible set); a calendar you subscribe to later appears automatically. The same checklist also lives **at the top of the Calendar panel's left rail** (a "Calendars" list, `CalendarVisibilityList.tsx`), so you can toggle a calendar without opening Settings — both surfaces write the same setting via the shared `nextHiddenIds` helper. Stored as `calendarHiddenIds` (the unchecked set; default empty = all shown). Backend: `resolveVisibleCalendarIds()` in `calendar-api-service.ts` reads across the visible set and tags each event with its `calendarId`.
- **Event popover** (`EventPopover.tsx`): click any event for its details with **Edit** and **Delete** actions; Delete goes through a confirm dialog.
- **Event editor** (`EventForm.tsx`): title, all-day switch, start/end, location, description, attendees as validated email chips, a recurrence builder (`RecurrenceBuilder.tsx`: daily / weekly / monthly / yearly presets or a custom rule with interval, weekdays, and end date), reminders (keep Google calendar defaults or add custom notification/email overrides from 5 minutes to 2 days before), and a target-calendar picker. Used for both create and edit.
- **Drag in Week view** (`useDragEvent.ts`): drag an event body to move it to a new time, or drag its bottom edge to resize; both persist through the store's `updateEvent`.
- **AI chat drawer** (`CalendarChatDrawer.tsx`): a persistent input bar at the bottom of the panel ("Ask about your calendar..."); expands into a 280px thread (Esc collapses). Answers run through the 6 calendar tools, and each write renders a labeled badge on the reply (Created / Updated / Deleted). Requires an API-key account; the `no_api_key` error is surfaced in plain language.
- **Sidebar Today/Tomorrow list** (`CalendarSidebarContent.tsx`, the **Calendar** tab of `CalendarPanelTabs` mounted by `SessionsSidebar` while the Calendar project is selected): today's and tomorrow's events with time, title, and location (URL locations are clickable); the current event, or one starting within 2 hours, is highlighted, and past events are dimmed.
- **Sessions tab** (`CalendarPanelTabs.tsx`, `calendar-session-host.ts`): the Calendar panel is a **Calendar | Sessions** toggle — the "Sessions" tab is the standard agent-session list ("+ New session", pause, archive, right-click menu, running-count badge), exactly like Tasks or Mind Map. A session started there is briefed as a calendar assistant and can view + manage your Google Calendar over the control server's `/google/calendar/events` routes (`GET`/`POST`/`PATCH`/`DELETE`); it runs in a managed `calendar-agent` workspace. See `.claude/memory/contracts/session-host-contract.md` and the `google-apis` omniscio-control surface.
- **Reconnect recovery**: an expired/revoked Google token during a Quick Launch calendar create renders a friendly error pane with an inline **Reconnect Google** button that reruns the unified OAuth flow and automatically retries the same create. That heal-in-place behavior is `create-errors-are-humanized-and-heal-in-place` in `.claude/memory/contracts/calendar-quick-add-invariants-contract.md`.

## How it behaves

### Background sync & notifications

Beyond on-demand reads, a background service ([/src/main/services/google/calendar-sync-service.ts](/src/main/services/google/calendar-sync-service.ts)) keeps your calendar fresh and drives notifications — while the Calendar integration is on, you're signed in, and you're online. It does two things:

- **Change detection (~every 5 min)** uses Google's incremental `syncToken` (its purpose-built "what changed" cursor) per visible calendar. When something changes it (a) fires a notice, and (b) pushes `CALENDAR_CHANGED` so any open calendar view reloads. A first sync is a silent baseline (no notices — every event would otherwise look new); an expired token (HTTP 410) is dropped and re-baselined.
  - **Two kinds of notice, two separate switches.** A brand-new event gets **"Event added: …"**; an edit to an existing one gets **"Calendar updated: …"**. These are different news — the first is a confirmation you stop wanting once you trust the feature, the second is something you probably still want to hear about — so each is its own alert type with its own switch (Settings → Notifications → Alert types → _Calendar event added_ / _Calendar event changed_). They used to be one type, which meant turning off the routine confirmations also silenced every real change. Google's incremental sync hands both over identically, so the two are told apart by comparing the event's creation and last-modified stamps (within ~2s of each other = newly added); when that cannot be determined the notice falls back to "Calendar updated", never to "added", so a real change someone else made is never routed into the switch you turned off for your own confirmations.
- **Reminders (~every 1 min)** fire a "starting soon" notice for each upcoming timed event crossing your lead time (default 15 min, configurable 0–120), at most once per occurrence (a small fire-once ledger survives restarts). All-day events are skipped.

**Delivery is your choice** — Settings → Notifications → **Google Calendar reminders**: a dismissible **inbox alert** (default), a transient **toast** pop-up, or **both**. "Added/changed" notices have their own sub-toggle (they can get chatty on a shared calendar). Settings: `calendarSyncEnabled`, `calendarNotificationsEnabled`, `calendarNotificationDelivery`, `calendarReminderMinutes`, `calendarChangeNotificationsEnabled`.

**Every "starting soon" card manages itself** — you never have to go hunting in Settings to stop them. The card carries **Turn off these reminders**, which flips `calendarNotificationsEnabled` off, toasts where to turn it back on, and archives the row — reminders ONLY, so calendar sync, the daily agenda card and the calendar views keep working, and the added / changed notices keep their own separate switches. Since every off switch now confirms first, the card tells you all of that BEFORE it acts rather than in a toast afterwards. Its primary button, **Open reminder settings**, deep-links straight to the Google Calendar reminders card in Settings → Notifications and flashes it, rather than dropping you at the top of the section.

This is deliberately **NOT a local calendar cache** — reads stay live against Google. Only two tiny operational tables back it (a per-calendar sync-token cursor + the fire-once ledger). Full invariants + the design rationale (why a cache was rejected): [calendar-sync-contract.md](/.claude/memory/contracts/calendar-sync-contract.md).

### Daily Agenda Card

Each morning the background service posts a **"Today's Agenda"** inbox card listing every event for the current day. Key facts:

- **On by default** for new installs (`calendarDailyAgendaEnabled: true`); only fires when `calendarEnabled` is also on.
- **Posts once per local day** — idempotent on its date-stamped `dedupKey daily-agenda-<YYYY-MM-DD>`: if today's card already exists (active OR already dismissed) it is NOT re-posted, so a restart never re-posts and an archived card is never resurrected. No fire-once ledger and no DB migration.
- **Scheduled time** is `calendarDailyAgendaHour:calendarDailyAgendaMinute` (default 7:00 AM local time). Configurable at **Settings → Calendar → Daily agenda card** (toggle) + **Agenda delivery time** (quarter-hour time picker).
- **Each event is a clickable https link** to its Google Calendar page (`htmlLink`). Events without an https link are listed as plain text.
- **An empty day** posts a friendly "nothing scheduled today" card — no card is ever suppressed for a free day. A calendar-read failure skips the card (never posts false-empty) and logs at `warn`.
- Inbox-only delivery — not routed through `calendarNotificationDelivery`.

### Clickable events in the sidebar

When the Calendar channel adapter surfaces events in the unified inbox/search (`ChannelProjectView.tsx`), each event message shows an **"Open in Google Calendar"** link below its body (the `ExternalLink` lucide icon in accent colour) when the event's `htmlLink` begins with `https://`. The link opens in the system browser via `openExternalUrl`. The field that carries the URL is `UnifiedMessage.externalUrl`, set in `mapEventToMessage` (calendar-adapter.ts); other channel adapters leave it undefined.

### How it works

OAuth lives in [/src/main/services/google/google-auth-service.ts](/src/main/services/google/google-auth-service.ts): single flow, loopback HTTP redirect, encrypted refresh token stored via `safeStorage`. The scopes come from `getOAuthScopes()`: `gmail.modify`, `gmail.settings.basic`, `calendar.events` + `calendar.calendarlist.readonly` + `calendar.freebusy` (the narrow Calendar trio — never the full `calendar` scope), `drive.file`, `spreadsheets`, `documents`, `contacts.readonly`, plus `meetings.space.readonly` only while the in-development Google Meet feature is switched on (Google's OAuth verification approves only scopes a released feature uses; locked by `tests/unit/services/google/google-auth-scopes.test.ts`). Each integration has a dedicated AI service with its own tool definitions exposed to Claude's API and a token-loop that runs: _agent message → Claude returns tool_use → Omniscio executes via googleapis → results appended → repeat until `stop_reason='end_turn'`_. The three services: [/src/main/services/google/calendar-ai-service.ts](/src/main/services/google/calendar-ai-service.ts) (6 tools: list, get, create, update, delete, check_availability), [/src/main/services/drive-ai-service.ts](/src/main/services/drive-ai-service.ts) (7 tools: list, get, create_folder, delete, move, share, quota), and [/src/main/services/sheets-ai-service.ts](/src/main/services/sheets-ai-service.ts) (10 tools: list, get, read_range, update_cells, create, add_sheet, delete_sheet, insert_rows, insert_cols, format). Each has a circuit breaker to prevent thundering-herd API storms, per-account cost tracking, and re-routes errors back as tool results so Claude can recover instead of crashing the turn. IPC surface: `CALENDAR_AI_CHAT`, `DRIVE_AI_CHAT`, `SHEETS_AI_CHAT` — wired by [/src/main/ipc/calendar-handlers.ts](/src/main/ipc/calendar-handlers.ts), [/src/main/ipc/drive-handlers.ts](/src/main/ipc/drive-handlers.ts), [/src/main/ipc/sheets-handlers.ts](/src/main/ipc/sheets-handlers.ts) (also exposing non-AI list/CRUD IPC). The read-only Drive MCP is force-disabled (it needed the restricted `drive.readonly` scope); [/src/main/services/mcp/mcp-registry.ts](/src/main/services/mcp/mcp-registry.ts) now only removes its old `~/.claude.json` entry. The bundled Google Workspace MCP is composed into each session's `.mcp.json` with NO secret in its env — it fetches a short-lived access token from `/mcp/credential` at call time. Integration-enable flags live in [/src/shared/types.ts](/src/shared/types.ts) under `AppSettings`.

## Related

- [gmail-integration.md](gmail-integration.md) — shares the same OAuth + token storage
- [mempalace-memory.md](mempalace-memory.md) — another example of Omniscio registering an MCP server for the CLI
