Skip to content

feat: Add AI calendar invite editor#169

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

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

Conversation

@mickn

@mickn mickn commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

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
Contributor 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 an AI-powered calendar invite editor to the sidebar: pressing i on a scheduling email triggers extraction of event details (title, guests, date/time, conference, location, notes, calendar) via the Anthropic API, opens a reviewable form inside the Calendar panel, and creates a Google Calendar event on confirmation. A new write OAuth scope (calendar.events) is added, with inline re-authentication when existing tokens lack it.

  • New invite editor flow: global keyboard shortcut i dispatches a nonce-stamped calendarInviteRequest to the store; CalendarPanel detects it and calls calendar:extract-invite IPC, then calendar:create-invite after review. The editor is modal — the tab bar and b shortcut are suppressed while the editor is open.
  • Timezone correctness: draft start/end are now floating wall-clock strings in the calendar timezone; timezone conversion happens at extraction boundaries using normalizeToCalendarWallClock and wallClockToInstant with a two-pass DST-aware fixed-point solver.
  • Demo mode: event extraction is bypassed in favor of a deterministic demo draft; created events are stored in-memory in demoCreatedEvents and merged with sample events when the IPC is polled.

Confidence Score: 5/5

Safe to merge. The invite creation flow, timezone handling, and modal locking are all implemented correctly; every regression flagged in the previous review round has been addressed with dedicated tests.

The feature is well-scoped: LLM extraction, timezone conversion, multi-day event editing, re-authentication, and keyboard modal locking are each handled carefully and covered by unit and e2e tests. The only new finding is a demo-mode sort inconsistency caused by mixing UTC ISO strings with floating wall-clock strings, which has no effect on real event creation or real users.

src/main/ipc/calendar.ipc.ts — draftToDemoEvent stores floating wall-clock strings while sample events use UTC ISO strings, causing an incorrect lexicographic sort order in non-UTC environments (demo only).

Important Files Changed

Filename Overview
src/extensions/mail-ext-calendar/src/renderer/CalendarPanel.tsx Core of the feature: adds invite state machine (extracting → ready → creating), InviteEditor component, proposed-event preview block, and modal locking of date nav/tab bar. The previous-thread issues (double-trigger, multi-day end date collapse, stale invite lock, calendar list wipe) are all addressed.
src/main/ipc/calendar.ipc.ts Adds calendar:extract-invite, calendar:create-invite, and calendar:get-invite-options IPC handlers. Demo mode has a sort inconsistency (sample events emit UTC ISO strings; demoCreatedEvents stores floating wall-clock strings), producing potentially incorrect overlap-column order in non-UTC environments. Real-event path is correct.
src/main/services/calendar-invite.ts New service: LLM extraction with retry, timeout handling, structured-output JSON schema, and demo fallback draft. Prompt-injection guard via UNTRUSTED_DATA_INSTRUCTION, per-provider timeouts, and a deterministic Meet requestId via FNV-1a hash.
src/shared/calendar-timezone.ts New module: floating wall-clock ↔ UTC instant conversions with DST-aware two-pass fixed-point. Handles midnight normalization, spring-forward gaps, and fall-back ambiguity with a well-commented residual caveat. Well tested.
src/shared/calendar-invite-editor.ts Pure wall-clock arithmetic helpers: updateInviteStartDate preserves multi-day duration, updateInviteEndTime preserves end date, updateInviteStartTime shifts end to maintain duration. Addresses all previously flagged multi-day edge cases.
src/shared/calendar-invite.ts Parsing, normalization, validation, and Google Calendar params building. Deterministic conferenceData requestId prevents duplicate Meet links on retry.
src/renderer/hooks/useKeyboardShortcuts.ts Adds 'invite' keyboard mode that suppresses single-key thread shortcuts while the editor is open; Escape cancels the invite via the global handler rather than a second listener in CalendarPanel.
src/renderer/components/EmailPreviewSidebar.tsx Hides tab bar while invite is pending, forces email tab on invite start (edge-triggered), and clears stale invite requests via isStaleInviteRequest when the user navigates to a different thread before CalendarPanel mounts.
src/extensions/mail-ext-calendar/src/google-calendar-client.ts Adds insertCalendarEvent, getCalendarList now returns writable flag based on token scopes and access role, and hasCalendarWriteScope helper reuses the already-loaded OAuth client.
src/shared/calendar-date.ts New utility: isoDateMatchesCalendarDate correctly handles timezone-aware comparison, replacing the old startsWith approach that broke for UTC-behind users.
tests/unit/calendar-invite-review-regressions.spec.ts New regression test suite covering the six issues flagged in the previous review: UTC date matching, multi-day end-time preservation, multi-day duration preservation, stale request detection, midnight rollover, and duration-preserving start-time shift.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant KBShortcuts as useKeyboardShortcuts
    participant Store as AppStore
    participant Sidebar as EmailPreviewSidebar
    participant Panel as CalendarPanel
    participant IPC as calendar.ipc (main)
    participant GCal as Google Calendar API

    User->>KBShortcuts: press "i"
    KBShortcuts->>Store: startCalendarInvite(emailId, threadId)
    Store-->>Sidebar: calendarInviteRequest set → hide tab bar, force email tab
    Store-->>Panel: calendarInviteRequest triggers effect → startInvite(emailId)
    Panel->>IPC: calendar:extract-invite
    IPC->>GCal: getCalendarList (for options + writable flag)
    IPC-->>Panel: draft + calendars + requiresReauth

    alt requiresReauth
        Panel-->>User: Show re-auth prompt
        User->>Panel: click Re-authenticate
        Panel->>IPC: calendar:get-invite-options (after reauth)
        IPC-->>Panel: updated calendars
    end

    User->>Panel: review and edit draft fields
    User->>Panel: click Create and send
    Panel->>IPC: calendar:create-invite
    IPC->>GCal: events.insert (with conferenceDataVersion if Meet)
    GCal-->>IPC: created event
    IPC->>IPC: saveCalendarEvents + broadcastCalendarUpdated
    IPC-->>Panel: success
    Panel->>Store: clearCalendarInviteRequest()
    Store-->>Sidebar: unlock tab bar
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
    participant User
    participant KBShortcuts as useKeyboardShortcuts
    participant Store as AppStore
    participant Sidebar as EmailPreviewSidebar
    participant Panel as CalendarPanel
    participant IPC as calendar.ipc (main)
    participant GCal as Google Calendar API

    User->>KBShortcuts: press "i"
    KBShortcuts->>Store: startCalendarInvite(emailId, threadId)
    Store-->>Sidebar: calendarInviteRequest set → hide tab bar, force email tab
    Store-->>Panel: calendarInviteRequest triggers effect → startInvite(emailId)
    Panel->>IPC: calendar:extract-invite
    IPC->>GCal: getCalendarList (for options + writable flag)
    IPC-->>Panel: draft + calendars + requiresReauth

    alt requiresReauth
        Panel-->>User: Show re-auth prompt
        User->>Panel: click Re-authenticate
        Panel->>IPC: calendar:get-invite-options (after reauth)
        IPC-->>Panel: updated calendars
    end

    User->>Panel: review and edit draft fields
    User->>Panel: click Create and send
    Panel->>IPC: calendar:create-invite
    IPC->>GCal: events.insert (with conferenceDataVersion if Meet)
    GCal-->>IPC: created event
    IPC->>IPC: saveCalendarEvents + broadcastCalendarUpdated
    IPC-->>Panel: success
    Panel->>Store: clearCalendarInviteRequest()
    Store-->>Sidebar: unlock tab bar
Loading

Reviews (13): Last reviewed commit: "Merge branch 'main' into codex/calendar-..." | 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
Contributor 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 Outdated
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
Contributor Author

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

claude and others added 2 commits July 3, 2026 19:16
Fixes surfaced by the PR review, focused on the invite editor being modal
in appearance but not in the input/state model:

- Make the invite a real keyboard mode. Escape is now handled once, centrally,
  in useKeyboardShortcuts (clearing the request) instead of a second window
  listener in CalendarPanel that double-fired and also deselected the thread.
  The new "invite" mode suppresses every single-key thread shortcut (s/e/#/j/k/b,
  and <select> type-ahead) while the editor owns the sidebar, and the central
  Escape is an always-mounted exit so the sidebar can't soft-lock if no
  CalendarPanel is mounted to consume the request.
- Slave the editor's open state to calendarInviteRequest: clearing the request
  (Escape, stale-thread safety net, or a completed create) tears the editor
  down, so it can no longer linger showing a stale draft after the user
  navigates to another thread.
- Editing only the start time now shifts the end to preserve the meeting
  duration (Google-Calendar behavior) via a shared, unit-tested
  updateInviteStartTime helper, instead of leaving the end at/before the new
  start (which blanked the preview and blocked creation).
- wallClockToInstant uses a two-pass DST fixed-point correction so bare times
  near a DST transition resolve to the correct instant; this feeds the sent
  event via normalizeToCalendarWallClock, not just the preview. Docstring
  corrected accordingly.
- getCalendarList derives write scope from the OAuth client it already holds
  instead of re-opening it (which re-read credentials/token files from disk).
- Remove a duplicated error-set block and a redundant setSelectedDate in
  startInvite.

Adds unit tests for the start-time duration behavior and the DST correction.
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.

3 participants