Google Integrations (Calendar, Drive, Sheets)
Calendar, Drive and Sheets behind one shared Connect Google consent — and all three are agent-facing, so you ask in plain English and Claude calls the right tool. What the Calendar panel shows and how syncing and notifications behave.
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
- 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. Your normal web browser opens at Google's sign-in (any passkey works there); 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.)
- 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 whilegmailEnabledis on and Calendar only whilecalendarEnabledis 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. - 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.
- Read the results. Drive operations come back with
webViewLinks 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. - Separately: Drive via MCP — force-disabled. This read-only Drive MCP (search/list/read/info/quota over the user's EXISTING Drive) needed the restricted
drive.readonlyscope, so it is now force-disabled and the shared OAuth requests onlydrive.file— see the scope-change note in 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-owngogCLI path (follow-up). - 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/markdownexport inlines every embedded image as a base64data: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 thosedata:image references (both reference definitions and inlineforms) 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. It applies only to the Docs-as-markdown path; Slides imported as PDF keep their visuals natively.
- Inline images are stripped from the imported markdown. Google's
- 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. - 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 ofPOST /gdoc/publish(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 samegoogleDocImportEnabledtoggle as step 6 (403FORBIDDEN—Google Doc import is off. Turn it on in Settings → Connections → Google Workspace.— when off). Docs only — a Slides/Sheets link returnsVALIDATION_FAILED; every other failure now carries the SHARED error vocabulary instead of the old ad-hocGDOC_IMPORT_*family (F557 maps each domain code onto it insrc/main/services/cli/cli-server-gdoc-routes.ts):FORBIDDENfor not-connected / no-access / expired-token / drive-not-authorized,NOT_FOUNDfor a missing doc,RATE_LIMITED, andSERVICE_UNAVAILABLEfor a timeout or a network failure — each with a plain message that never leaks 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.
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 incalendar-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 opens from the gear at the right edge of the Calendar panel's tab row — the panel's own settings, a small "Calendar settings" panel holding the "Calendars" list (
CalendarPanelTabs.tsx+CalendarVisibilityList.tsx), so you can toggle a calendar without opening Settings. The gear sits in the tab row rather than inside the list, so hiding every calendar can never leave you with no way back. Both surfaces write the same setting via the sharednextHiddenIdshelper. Stored ascalendarHiddenIds(the unchecked set; default empty = all shown). Backend:resolveVisibleCalendarIds()incalendar-api-service.tsreads across the visible set and tags each event with itscalendarId. - 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'supdateEvent. - AI chat drawer (
CalendarChatDrawer.tsx): a persistent ask bar at the bottom of the panel ("Ask about your calendar..."); expands into a 280px thread (Esc collapses). The bar is the app's chat bar — the sameMessageBarComposerthe session composer renders — so it auto-grows as you type, supports text undo/redo and markdown paste, and sends on your configured Enter / Ctrl+Enter chord (Shift+Enter is a newline). When FlowVoice is enabled its dictation mic rides in the bar's left strip. 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; theno_api_keyerror is surfaced in plain language. - Sidebar Today/Tomorrow list (
CalendarSidebarContent.tsx, the Calendar tab ofCalendarPanelTabsmounted bySessionsSidebarwhile 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/eventsroutes (GET/POST/PATCH/DELETE); it runs in a managedcalendar-agentworkspace. See.claude/memory/contracts/session-host-contract.mdand thegoogle-apisomniscio-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-placein.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) 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) pushesCALENDAR_CHANGEDso 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.
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 whencalendarEnabledis 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: 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). gmail.modify and gmail.settings.basic are Google restricted scopes, so the app owes Google's restricted-scope verification plus an annual CASA security assessment; oauth-scopes-casa-tier2.test.ts scans all of src/ and allows no other restricted scope. 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 (6 tools: list, get, create, update, delete, check_availability), /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 (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/drive-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 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 under AppSettings.
Related
- gmail-integration.md — shares the same OAuth + token storage
- mempalace-memory.md — another example of Omniscio registering an MCP server for the CLI
Last verified 2026-10-06