Skip to content

feat: Add AI calendar invite editor#169

Open
mickn wants to merge 12 commits into
ankitvgupta:mainfrom
mickn:codex/calendar-invite-editor-clean-pr
Open

feat: Add AI calendar invite editor#169
mickn wants to merge 12 commits into
ankitvgupta:mainfrom
mickn:codex/calendar-invite-editor-clean-pr

Conversation

@mickn

@mickn mickn commented Jun 2, 2026

Copy link
Copy Markdown

Summary

This adds an AI-powered calendar invite editor to the Calendar sidebar. From a scheduling email, Exo can extract the likely invite automatically, fill the editable fields, and let the user review everything before creating the Google Calendar event.

The extraction pulls out the title, guests, date, time, meeting type, location, notes, and target calendar. If calendar write permission is missing, the sidebar shows the re-authentication state directly in the invite flow.

Screenshots

All screenshots use demo data.

Calendar sidebar with scheduling email

AI-extracted calendar invite editor

Calendar write permission re-authentication state

Changes

  • Adds calendar invite extraction from the current email thread.
  • Adds a reviewable invite editor in the Calendar sidebar.
  • Supports editable guests, date/time, conference details, location, notes, and calendar selection.
  • Handles missing Google Calendar write permission with an inline re-authentication prompt.
  • Keeps demo mode non-live for the invite flow and PR screenshots.
  • Keeps pre-PR agentic verification traces local instead of publishing transcripts to GitHub comments.

⚠️ Behavior change: Google Calendar write scope

This PR adds https://www.googleapis.com/auth/calendar.events to the OAuth scopes requested at sign-in (needed to create events / Google Meet conferences). Because the scope is new, existing accounts must re-authenticate before they can create invites — their current tokens only carry the read scope. The invite flow detects this and shows an inline "Re-authenticate" prompt rather than failing silently, but it is a one-time re-consent for every already-signed-in user. Read-only calendar viewing is unaffected in the interim.

Revision (timezone correctness + modal UX + Meet clarity)

A follow-up commit on this branch addresses review feedback and a timezone bug:

  • Timezone correctness. Draft start/end are now floating wall-clock strings interpreted in the draft's own timezone (the representation Google's { dateTime, timeZone } consumes), instead of UTC instants. This removes a bug where editing a time converted it in the browser's zone but stamped it with the calendar's zone, shifting the event when the two differed. New src/shared/calendar-timezone.ts converts at the boundaries: explicit-offset times from the email (e.g. "2pm London") are normalized into the calendar zone; bare times fall back to the user's physical zone, then are expressed in the calendar zone. The extraction prompt now emits an offset only when the email states a zone, leaving conversions to code.
  • Invite editor is now modal. While the editor is open, the sidebar tab bar and the calendar date navigation are hidden and the b tab-switch shortcut is suppressed, with an explicit Close (×) button in the header. Previously those controls stayed visible but were silently reverted, so they read as broken when clicked.
  • Google Meet clarity. Selecting Google Meet now shows "A Google Meet link will be created and added to the invite" instead of an empty disabled field, and the Meet requestId is derived deterministically from the draft so a retried create reuses the same conference instead of minting a duplicate.

Validation

  • npm run build
  • npm run typecheck
  • npm run lint
  • npm exec -- playwright test --project=unit tests/unit/runtime-flags.spec.ts tests/unit/calendar-invite.spec.ts
  • npm exec -- playwright test --project=e2e tests/e2e/calendar-invite.spec.ts
  • npm run pre-pr -- --quick --no-inject --no-comment

Pre-PR verdict: PASS

  • mode: full
  • sha: 20bda0f
  • generated: 2026-06-02T22:29:25.836Z
Phase Status Duration
eval:analyzer ✅ exit 0 14.0s
eval:features ✅ exit 0 28.7s
agentic-verify ✅ exit 0 70.8s
real-gmail:cached ✅ exit 0 2.3s

@mickn

mickn commented Jun 2, 2026

Copy link
Copy Markdown
Author

✅ Pre-PR verification — PASS

  • mode: full
  • sha: 20bda0f
  • generated: 2026-06-02T22:29:27.438Z
Phase Status Duration
eval:analyzer ✅ exit 0 14.0s
eval:features ✅ exit 0 28.7s
agentic-verify ✅ exit 0 70.8s
real-gmail:cached ✅ exit 0 2.3s
Agentic verification — local report

Agentic verification wrote local artifacts. Their contents are not posted to GitHub because the verifier can inspect real mailbox/calendar data.

  • Markdown: scripts/.agentic-runs/2026-06-02T22-28-12-955Z-verify-diff.md
  • JSON: scripts/.agentic-runs/2026-06-02T22-28-12-955Z-verify-diff.json
  • Trace: scripts/.agentic-runs/2026-06-02T22-28-12-955Z-verify-diff.log

This comment is upserted by npm run pre-pr. The CI gate reads the marker block in the PR description, not this comment.

This was referenced Jun 2, 2026
@mickn mickn marked this pull request as ready for review June 2, 2026 22:35

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment thread src/main/ipc/calendar.ipc.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a full AI-powered calendar invite editor to the Calendar sidebar. An extraction service uses an LLM to pull meeting details from an email thread, populates an editable form, handles Google Calendar write-scope re-authentication inline, and finally creates a Google Calendar event. A follow-up commit in the same branch also addressed previously raised review concerns — timezone correctness (floating wall-clock + timeZone pair), multi-day duration preservation, stale invite-lock cleanup, and the reauth/options-fetch failure guard.

  • New invite flow: i hotkey triggers extraction via calendar:extract-invite IPC; InviteEditor renders a title/guests/date/time/conference/location/notes/calendar form; calendar:create-invite IPC creates the event and broadcasts an update.
  • Timezone overhaul: src/shared/calendar-timezone.ts canonicalises LLM output into floating wall-clock strings in the calendar zone; wallClockToInstant converts them back to instants for the preview grid.
  • Modal UX: while the editor is open the sidebar tab bar, date navigation, and b shortcut are suppressed; stale requests for a different thread are cleared by EmailPreviewSidebar.

Confidence Score: 5/5

Safe to merge. The core invite extraction, timezone handling, and calendar creation paths are well-guarded, and all six issues raised in the previous review round appear to be addressed.

The new extraction, timezone, and creation logic is thoroughly tested (unit + e2e specs), and all previously flagged multi-day date, stale-lock, reauth-failure, and double-trigger bugs have been fixed. The two remaining findings are minor: a redundant state setter call that has no user-visible effect, and a keyboard shortcut that lacks a guard already applied to its sibling.

CalendarPanel.tsx (duplicate error setter, i-shortcut re-trigger) and useKeyboardShortcuts.ts (i shortcut guard) are the only files with nits; no files require blocking attention.

Important Files Changed

Filename Overview
src/extensions/mail-ext-calendar/src/renderer/CalendarPanel.tsx Core UI for the invite editor. Adds ~900 lines for InviteEditor component, startInvite/cancelInvite/createInvite callbacks, and proposed-event preview. Contains a duplicate setInviteError call in startInvite (lines 1034–1036 and 1057–1059) and the i shortcut can re-trigger extraction when the editor is open and focus is on a non-input element.
src/main/services/calendar-invite.ts New LLM extraction service. Prompt safety via wrapUntrustedEmail/UNTRUSTED_DATA_INSTRUCTION, retry logic, deterministic conference requestId derivation, and a well-structured demo fallback. No issues found.
src/main/ipc/calendar.ipc.ts Adds three new IPC handlers (get-invite-options, extract-invite, create-invite) and demo-mode stubs. Server-side writability re-check before insert, Zod schema validation of incoming draft, and proper broadcast of calendar updates. No issues found.
src/shared/calendar-timezone.ts New timezone utility. Converts between floating wall-clock strings and instants using Intl.DateTimeFormat. Correctly handles DST edges (documented caveat for preview only), explicit-offset LLM output, and bare wall-clock interpretation in user zone. No issues found.
src/shared/calendar-invite-editor.ts Editor helpers — combineDateAndTime, updateInviteStartDate (preserves multi-day duration), updateInviteEndTime (preserves end date, rolls midnight), shouldStartInviteExtraction, isStaleInviteRequest. Correctly avoids Date.getTime() for wall-clock arithmetic. No issues found.
src/renderer/hooks/useKeyboardShortcuts.ts Adds i shortcut to start a calendar invite and guards b when invite is open. The i shortcut lacks the same guard as b, so it can re-trigger extraction when the editor is open and a non-input element has focus.
src/renderer/components/EmailPreviewSidebar.tsx Adds invite-pending detection to hide tab bar while editor is open, forces email tab on invite open, and clears stale calendarInviteRequest when the user navigates to a different thread. No issues found.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    actor User
    participant KS as useKeyboardShortcuts
    participant Store as AppStore
    participant ESP as EmailPreviewSidebar
    participant CP as CalendarPanel
    participant IPC as calendar.ipc (main)
    participant LLM as LLM Service
    participant GCal as Google Calendar API

    User->>KS: Press "i"
    KS->>Store: startCalendarInvite(emailId, threadId, nonce)
    Store-->>ESP: calendarInviteRequest set → hide tab bar, force email tab
    Store-->>CP: calendarInviteRequest set
    CP->>CP: shouldStartInviteExtraction → true
    CP->>IPC: calendar:extract-invite(emailId)
    IPC->>IPC: getEmail + getEmailsByThread
    IPC->>IPC: getInviteCalendarOptions()
    IPC->>LLM: extractCalendarInviteDraft(threadEmails, calendars)
    LLM-->>IPC: raw JSON draft
    IPC->>IPC: parseCalendarInviteDraft → normalizeToCalendarWallClock
    IPC-->>CP: "{ draft, calendars, requiresReauth }"
    CP->>CP: render InviteEditor (pre-filled)

    alt User edits and submits
        User->>CP: Edit fields, click Create and send
        CP->>CP: validateCalendarInviteDraft (client-side)
        CP->>IPC: calendar:create-invite(accountId, draft)
        IPC->>IPC: CalendarInviteDraftSchema.safeParse + server validate
        IPC->>GCal: insertCalendarEvent(params)
        GCal-->>IPC: created event
        IPC->>IPC: saveCalendarEvents + calendarSyncService.syncNow()
        IPC-->>CP: "{ success: true, event }"
        CP->>Store: clearCalendarInviteRequest()
        Store-->>ESP: tab bar restored
    else Missing write scope
        IPC-->>CP: "{ requiresReauth: true }"
        User->>CP: Click Re-authenticate
        CP->>IPC: window.api.auth.reauth(accountId)
        IPC-->>CP: success
        CP->>IPC: calendar:get-invite-options
        IPC-->>CP: updated calendars (writable: true)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    actor User
    participant KS as useKeyboardShortcuts
    participant Store as AppStore
    participant ESP as EmailPreviewSidebar
    participant CP as CalendarPanel
    participant IPC as calendar.ipc (main)
    participant LLM as LLM Service
    participant GCal as Google Calendar API

    User->>KS: Press "i"
    KS->>Store: startCalendarInvite(emailId, threadId, nonce)
    Store-->>ESP: calendarInviteRequest set → hide tab bar, force email tab
    Store-->>CP: calendarInviteRequest set
    CP->>CP: shouldStartInviteExtraction → true
    CP->>IPC: calendar:extract-invite(emailId)
    IPC->>IPC: getEmail + getEmailsByThread
    IPC->>IPC: getInviteCalendarOptions()
    IPC->>LLM: extractCalendarInviteDraft(threadEmails, calendars)
    LLM-->>IPC: raw JSON draft
    IPC->>IPC: parseCalendarInviteDraft → normalizeToCalendarWallClock
    IPC-->>CP: "{ draft, calendars, requiresReauth }"
    CP->>CP: render InviteEditor (pre-filled)

    alt User edits and submits
        User->>CP: Edit fields, click Create and send
        CP->>CP: validateCalendarInviteDraft (client-side)
        CP->>IPC: calendar:create-invite(accountId, draft)
        IPC->>IPC: CalendarInviteDraftSchema.safeParse + server validate
        IPC->>GCal: insertCalendarEvent(params)
        GCal-->>IPC: created event
        IPC->>IPC: saveCalendarEvents + calendarSyncService.syncNow()
        IPC-->>CP: "{ success: true, event }"
        CP->>Store: clearCalendarInviteRequest()
        Store-->>ESP: tab bar restored
    else Missing write scope
        IPC-->>CP: "{ requiresReauth: true }"
        User->>CP: Click Re-authenticate
        CP->>IPC: window.api.auth.reauth(accountId)
        IPC-->>CP: success
        CP->>IPC: calendar:get-invite-options
        IPC-->>CP: updated calendars (writable: true)
    end
Loading

Reviews (11): Last reviewed commit: "Preserve calendar list when post-reauth ..." | Re-trigger Greptile

Comment thread src/extensions/mail-ext-calendar/src/renderer/CalendarPanel.tsx Outdated
Comment thread src/extensions/mail-ext-calendar/src/renderer/CalendarPanel.tsx
Comment thread src/main/ipc/calendar.ipc.ts
@mickn mickn changed the title [codex] Add AI calendar invite editor feat: Add AI calendar invite editor Jun 3, 2026
Comment thread src/extensions/mail-ext-calendar/src/renderer/CalendarPanel.tsx
@ankitvgupta

Copy link
Copy Markdown
Owner

very cool! yeah you can use /reviewloop to satisfy the bots but iv been meaning to do this for a while.

Comment thread docs/superpowers/specs/2026-05-28-calendar-invite-editor-design.md Outdated
Comment thread scripts/agentic-verify.mjs
Comment thread scripts/pre-pr.mjs
Comment thread src/main/demo/fake-inbox.ts Outdated
Comment thread tests/unit/ollama-cloud-routing.spec.ts Outdated
Comment thread docs/LOCAL_DEVELOPMENT.md Outdated
@ankitvgupta

Copy link
Copy Markdown
Owner

i like this feature but there are a bunch of changes in this PR that are unrelated to the feature so please move those to their own PRs.

@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

mickn and others added 2 commits June 16, 2026 10:13
Timezone correctness:
- Represent draft start/end as floating wall-clock in the draft's own
  timezone (what Google's { dateTime, timeZone } pair consumes) instead of
  UTC instants, removing the editor's local-zone round-trip that shifted
  times when the calendar zone differed from the browser zone.
- Add src/shared/calendar-timezone.ts: convert an extracted value into the
  calendar wall-clock. Explicit-offset times ("2pm London", +01:00) are
  converted to the calendar zone; bare times fall back to the user's
  physical zone, then are expressed in the calendar zone.
- Update the extraction prompt to emit an offset only when the email states
  a zone (floating otherwise) so code, not the LLM, does conversions.

Invite UX is now modal:
- Hide the sidebar tab bar and calendar date-nav while the invite is open;
  suppress the "b" tab-switch shortcut. Previously these stayed visible and
  appeared clickable but were silently reverted, reading as broken.
- Add an explicit Close (X) button to the editor header.

Google Meet clarity:
- When Google Meet is selected, show "A Google Meet link will be created and
  added to the invite" instead of an empty disabled field.
- Derive a stable conference requestId from the draft so a retried create
  reuses the same Meet conference rather than minting a duplicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Note that the single offset correction is preview-only and the imprecision
at DST transitions never reaches the event sent to Google.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/extensions/mail-ext-calendar/src/renderer/CalendarPanel.tsx
mickn and others added 2 commits June 16, 2026 10:50
ExtensionPanelSlot hardcoded `extensionId === "calendar" && panelId ===
"day-view"` to suppress its generic title bar — calendar-specific knowledge
in a generic extension primitive. Replace it with an `ownHeader` capability
declared in the panel manifest (SidebarPanelContributionSchema), threaded
through registration and getSidebarPanels to the slot. The calendar day-view
sets `ownHeader: true`; any panel that renders its own chrome can now opt in
without the slot knowing which extension it is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From code-review feedback. These are the in-scope correctness/security fixes;
the larger semantic-pipeline redesign is tracked separately as a follow-up.

- updateInviteStartDate: don't blank the end time when the existing end has no
  parseable time — fall back to a 30-minute duration off the new start.
- updateInviteEndTime: roll the end to the next day when the chosen end time
  lands at/before the start (events crossing midnight).
- chooseDefaultCalendar: never default to a read-only calendar; return null so
  validation/re-auth handles the no-writable-calendar case.
- Extraction prompt: mark calendar records as data, not instructions (shared/
  subscribed calendar names are attacker-influenceable); the chosen calendarId
  is already re-validated server-side before any Google API call.
- Extraction request: set temperature 0 for deterministic structured output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mickn

mickn commented Jun 16, 2026

Copy link
Copy Markdown
Author

Code-review follow-up: now vs. deferred

A detailed review proposed shifting the invite extractor toward a "LLM as semantic judge over structured facts; deterministic code resolves/validates/blocks" architecture. The core thesis is right, and I split the response:

Landed in this PR (commit 4dd6b69) — in-scope

  • Editor end-time blanking bug: changing the start date no longer wipes the end when the existing end has no parseable time (falls back to +30 min).
  • Midnight-crossing end time: setting end ≤ start (e.g. 23:30 → 00:30) now rolls the end to the next day.
  • Read-only default calendar: chooseDefaultCalendar no longer falls back to a non-writable calendars[0]; returns null so validation/re-auth handles it.
  • Untrusted calendar names: the system prompt now marks calendar records as data, not instructions (shared/subscribed names are attacker-influenceable). The chosen calendarId was already re-validated server-side before any Google API call.
  • temperature: 0 for deterministic extraction.

All covered by new unit tests (33 calendar unit tests pass).

Deferred to a follow-up (deliberately not in this PR, since it’s a new feature, not a fix)

A larger redesign that would change the schema, cost profile, and UX, and needs a golden eval set to validate:

  • Three-stage pipeline: deterministic context builder → LLM semantic extraction → deterministic resolver/validator (+ optional verifier pass).
  • intent / readiness fields and candidateSlots to represent unresolved/multi-slot/reschedule/cancel threads instead of forcing a single start/end.
  • Candidate-ID selection for emails/URLs/phones so the model selects rather than reproducing exact strings (prevents subtly corrupted Zoom links / addresses).
  • Move offset/timezone resolution fully into code (model emits timezoneText+IANA guess, not a computed +01:00); evaluate Temporal/Luxon.
  • Validation-driven retry/repair (pass structured validation errors back), all-day/recurring support, smarter middle-out truncation that preserves candidate facts from all messages.

Net: the LLM should own semantic interpretation; exactness (timezones, exact strings, calendar IDs, validation) belongs in deterministic code. This PR moves further in that direction; the full pipeline is the next step.

The screenshots referenced in the PR body 404'd — they pointed at a now-gone
repo (mickn/exo-calendar-invite-pr). Re-capture from the demo app and commit
them under docs/pr-assets/ on this branch so the references are stable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/extensions/mail-ext-calendar/src/renderer/CalendarPanel.tsx
mickn and others added 2 commits June 16, 2026 11:29
Greptile caught a soft-lock introduced by the modal invite UX: pressing `i` on
thread A then selecting an email in thread B before extraction starts leaves
calendarInviteRequest non-null with no invite editor rendered — the sidebar tab
bar stays hidden and `b` stays suppressed with no reachable exit.

EmailPreviewSidebar (always mounted) now clears the request when it targets a
thread other than the selected one, via a new isStaleInviteRequest predicate
(keyed on a confirmed different selected thread so it can't race the legitimate
same-thread start). CalendarPanel's effect is left to only start same-thread
requests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Greptile P1: reauthenticateInviteCalendar wiped inviteCalendars and reset
requiresReauth to false before checking whether getInviteOptions succeeded. On
a failed fetch this emptied the dropdown AND hid the Re-authenticate button,
stranding the user with no writable calendar and no retry path. Now the
calendar list and re-auth state are only updated on a successful fetch; on
failure we surface the error and leave prior state intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mickn

mickn commented Jun 16, 2026

Copy link
Copy Markdown
Author

fyi: all comments are resolved, and non-relevant changes moved out of this pr

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants