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

Calendar Quick Add (part 2)

Part 2 of the Calendar Quick Add page: why the parser is regex-first with the AI as a rare fallback, why the preview shows more than one row only when the input is genuinely ambiguous, and the repo-aware code map underneath the tab.

What it is

This is part 2 of the Calendar Quick Add page. It covers why the tab behaves the way it does, decision by decision, and then the code map underneath it: the parser, the time-zone conversion, the calendar picker, the fallback AI call, the IPC channels and the daily cost cap.

Where to find it

There is nothing new to open here. Every decision below is about the same one surface: the Calendar tab of the Quick Launch composer, opened with Ctrl+Space and described step by step on the parent page. The daily fallback cap and the saved default calendar are the only two pieces of state it keeps.

How it behaves

Design decisions worth knowing

  1. Text-first, with a Repeat checkbox as a fallback (2026-05-24 pivot; checkbox added 2026-07-06) — the original UI had a separate free-text Repeat field, but the inline recurrence detector already handles "every weekday" / "yearly" / "every other Tuesday" from the same line. Two free-text fields where one sufficed produced inconsistent results (an Item and a Repeat that disagreed) and was more UI to read, so the free-text Repeat field was dropped — the typed line is the source of truth for the title and the RRULE. What came back later is different in kind: a small structured Repeat checkbox + frequency picker (not a second text box) that stays out of the way until you need to force a repeat the text didn't imply. It mirrors any parsed recurrence, so it can never silently disagree with the preview (see #11).

  2. Regex first, LLM only on miss — and the AI is meant to be rare (~<1%) — after the Phase 2 layer extensions (date-anywhere scan covering "tomorrow 10am birthday", "10am tomorrow gym", "meeting at 3 today", plus slang anchors like tonite/tmrw/2morrow), the anchored regex handles the overwhelming majority of inputs with $0 spend. Haiku is the fallback for the long-tail prose the regex misses. The "AI is thinking…" pill exists so when you do hit that ~1% case you understand why the parse took an extra second — not because the feature is slow, but because your phrasing was unusual.

  3. Multi-row preview only when the input is genuinely ambiguous (2026-05-25 tightening) — "Friday" is ambiguous (is it the whole day or a morning thing?) so Omniscio surfaces an all-day row and a 9 AM timed alternative. But "Birthday tomorrow at 2pm" is not ambiguous — the user typed the exact time — so the parser emits a single row instead of the pre-2026-05-25 trio that also showed an all-day alternative on the same date and a 30-minute look-alike that rendered identically to the 1-hour default. The dedupe layer applies to the LLM fallback path too so the same look-alike collapse holds when Haiku produces the rows.

  4. Fixed window; tall previews scroll (changed 2026-08-01) — the Control Space window is one fixed 720×660 size for every tab; the old grow-to-fit resize was deleted because any live window grow exposed a strip the compositor cleared white for a frame (the recurring flash bug — entrance-animation contract I21). A preview taller than the window scrolls, and the Cancel/Create footer is pinned OUTSIDE the scroll region (a flex-shrink-0 sibling), so it is structurally impossible for the window edge to clip it — content scrolls, the buttons stay put (contract I34; the 2026-07-07 fix for a stale height mirror that under-sized the window). With the 2026-05-25 dedupe + exact-time-no-all-day tightening, most previews fit without scrolling anyway.

  5. Pinned by default, with a startup migration — Calendar Quick Add is high-frequency for users with Google connected, so the new tab needs to be visible without manual pinning. The one-shot migration backfills existing installs once; the sentinel calendarQuickAddBackfilledAt prevents it from re-running every launch.

  6. Main-window toast, not an in-window pane (changed 2026-06-23) — a successful create closes the overlay immediately and confirms with a small bottom-left toast in the main Omniscio window (Open + Undo), instead of the old in-window pane that lingered for 4 seconds. Because the overlay is a separate window, the toast is relayed to the main window (QUICK_LAUNCH_CALENDAR_NOTIFY_SUCCESS → CALENDAR_QUICK_ADD_CONFIRMED) — a renderer can't fire a toast in another window. Undo is one click (with a "Removed" confirmation); no re-prompt.

  7. Calendar picker (2026-07-01) — the tab now offers a calendar chooser and remembers your pick as the default (calendarQuickAddDefaultCalendarId). Two deliberate constraints keep it from being clutter: it renders only when you have 2+ writable calendars (a single-calendar account never sees it), and it filters to calendars you can actually write to (via each calendar's accessRole from the Google API) so a create never silently fails on a read-only feed. Primary stays the default until you choose otherwise. This supersedes the original "primary only, use the Calendar AI chat instead" stance.

  8. "…of the month" without "every" offers repeating-vs-once, not whole-day-vs-morning (2026-06-02) — "Team meeting first Monday of the month" genuinely could mean a recurring monthly meeting or a single upcoming event, so Omniscio surfaces both rather than guessing. The two rows land on the real next occurrence of the pattern (computed by firstMonthlyOccurrence), never on now and never on a bare "next Monday" — an earlier draft mis-read the bare phrase as a one-shot on the next plain weekday with no recurrence at all. For these anchored-date phrases the all-day/9 AM alternative is dropped: the row budget is better spent on the recurring-vs-once decision, and a specific calendar anchor makes the morning-slot guess noise. Adding "every" removes the ambiguity and collapses to a single recurring row.

  9. A high-precision date scanner runs first, so a date can sit anywhere and never leaks into the title (2026-06-14) — the original parser could only spot a date at the very end of what you typed; anything after it (a time, an event name) made the date invisible, so the parser silently fell back to today and dumped the date text into the title. A dedicated scanner now finds an explicit date (numeric, ISO, month-name, bare day-of-month, weekday+day) wherever it sits and runs before the older suffix walk — so "6/21 12pm vr event" books June 21 with the clean title "Vr event" instead of "today" with "6/21 vr event" stuck in the title. It is deliberately precise over greedy: a lone number is never a date (no accidental events from "invoice 4521" or "room 1530"), and a bare dash/dot needs a year ("sprint 2-3" isn't a date). Two product calls shaped it — "next Sunday" skips to next week's Sunday (the consistent rule for every "next <weekday>"), and when a weekday and a day-of-month disagree ("Mon the 21st" when the 21st is a Sunday) Omniscio shows both dates rather than guessing. The whole change lives in the calendar layer; the shared snooze engine that powers session-snooze and alarms is untouched.

  10. Typed time zones are honored, not dropped (2026-07-01) — the parser used to strip a zone token (EST) and book the event in your computer's zone, silently ignoring what you wrote — wrong for anyone not already in that zone. Now the zone is captured and the event is created at that wall-clock in that IANA zone and tagged with it, so "9am EST" is 09:00 America/New_York regardless of the host. The conversion goes through a small DST-correct shared helper (wall-clock-zone.ts) that recomputes the instant with the target zone's real offset for that date, and it emits an offset-bearing ISO so the strict create-event schema (which requires an offset) is untouched. Coverage is abbreviation→IANA for the common US zones + UTC/GMT; anything unrecognized still falls through to the AI parser, and no-zone input is byte-identical to the old host-zone behavior. Reported via in-app feedback (a Pacific-time user whose "EST" events landed at the wrong hour).

  11. A dangling "every" is understood, and a Repeat box is the fallback (2026-07-06) — typing the day first and the repeat last ("Kathleen Tuesday 10am every") used to leave the bare word "every" stranded in the title with no recurrence, because the parser only recognized a complete phrase like "every Tuesday". Now, when a leftover "every" / "every other" has a weekday to anchor it, the parser reads it as every [other] <that weekday> — built from the day the event actually lands on — and produces a single weekly / biweekly row; with no weekday it just cleans the stray word out of the title. This only kicks in after the normal recurrence match misses, so nothing that already parsed changes (verified against the full ~1,500-phrase test set with zero regressions). For everything the text still can't express — an exact date you want to repeat, an unusual phrasing — a small Repeat checkbox + frequency picker below the preview lets you force it. The box is renderer-only (it reuses the same recurrence builder the parser uses) and defaults to mirroring any recurrence the text already implied, so it never disagrees with the preview. Reported via a screenshot of "Kathleen Tuesday 10:00 AM every" previewing as the title "Kathleen every".

For agents

Where each piece lives: the tab component, the action registry entry, the parser and its date scanner, the shared clock primitive and time-zone helper, the calendar picker, the fallback AI service, the IPC channels, the daily cost cap, the startup migration, the voice mic and the UI anchors. Behaviour, decisions and rationale are above; this section is the map of the code that produces them.

How it works (for repo-aware readers)

  • Tab component: src/renderer/src/features/quick-launch/QuickLaunchCalendarTab.tsx — owns the single item state plus usingLlm (CALENDAR_PARSE_LLM_STARTED listener), debounced parse (300 ms), preview row selection, ResizeObserver-driven window-height request. On a successful create it relays QUICK_LAUNCH_CALENDAR_NOTIFY_SUCCESS and closes the overlay — the confirmation toast (Open + Undo) is shown by the main window, not an in-window pane. No Zustand (Quick Launch is its own renderer process with no store sharing). A renderer-only Repeat control (repeatMode: 'auto' | 'off' | RepeatFreq + the exported effectiveRepeatRrule(row, mode)) overrides the recurrence applied to the previewed/submitted row via the shared parseRecurrencePhraseToRrule — no parser, IPC, or backend change; 'auto' mirrors the parsed rrule so unchecked is byte-identical to before (contract the-repeat-control-is-a-renderer-only-override).
  • Action registry: declared in src/shared/quick-launch-actions.ts with id: 'calendar', iconName: 'Calendar', enabledSelector: (s) => s.calendarEnabled. Pinned by default — calendar is the last entry in DEFAULT_QUICK_LAUNCH_PINNED_ACTION_IDS. Component map in src/renderer/src/features/quick-launch/quick-launch-action-components.ts.
  • Parser (regex-first, ~99% coverage): src/shared/calendar-quick-add-parser.ts — anchored-regex layers (snooze-parser style) for date/time/duration extraction PLUS — running FIRST, ahead of the suffix walk and the day+clock scan — a high-precision explicit-date scanner (src/shared/calendar-date-scan.ts scanDate) that locates a calendar date anywhere in the body (numeric M/D / M/D/Y, ISO Y-M-D, dash/dot M-D-Y with an explicit year, month-name ± ordinal in either order, a bare day-of-month with an ordinal, and weekday+day-of-month) and resolves it to a local-midnight date; the residual keeps any clock and becomes the title. It must run with priority because the suffix walk would otherwise grab a trailing clock-only span (6/21 12pm vr event → the clock → today) or let the snooze engine misread sun 21 as 21:00. Precision over recall: a lone integer is never a date (invoice 4521, or a whole-body 15 / 1530 / 60), next <weekday> resolves to the occurrence after the nearest, and a weekday + day-of-month that disagree emit BOTH dates (via: 'weekday-daynum-conflict'). The older Phase-2 findDateAnywhere scan (day-token + clock-token composition — "tomorrow 10am birthday", "10am tomorrow gym", "meeting at 3 today") remains as a fallback after the suffix walk and also rescues a stranded day word from the title ("lunch tomorrow at noon" → "Lunch"). The shared snooze engine is untouched. Recurrence detection in src/shared/calendar-quick-add-rrule.ts produces full RRULE strings (RRULE:FREQ=WEEKLY;BYDAY=MO,FR;COUNT=10). A dangling "every" / "every other" the complete-phrase matcher (INLINE_RECURRENCE_RE) misses is caught in Step 2 by the end-anchored DANGLING_RECURRENCE_RE, stripped from the title, and — when a weekday survives — synthesized in Step 5 into every [other] <weekday> from the RESOLVED event day (so a redundant "Ruby Tuesday … Friday" can't mis-anchor), giving a single weekly/biweekly row; it fires only after the inline match misses (contract a-dangling-every-is-a-recurrence). Returns up to 2 QuickAddInterpretation rows ranked by confidence — 1 when the input carried an exact clock, 2 (all-day + 9 AM alternative) when the input was date-only, and 2 (recurring + one-shot) for an anchored-monthly "…of the month" phrase typed without "every". For those anchored-monthly phrases the event start is computed from firstMonthlyOccurrence(rrule, now) (the next real occurrence — first Monday of the month lands on the upcoming first-Monday, not a bare "next Monday") and the all-day/9 AM pair is suppressed in favour of the repeating-vs-once choice. The exported dedupeInterpretations(rows) helper collapses any two rows whose render-identity (title|allDay|startIso|rrule) matches — the recurrence label is painted on a second line, so a recurring row and a one-shot row on the same date stay distinct — so an LLM-fallback path that synthesizes visually-identical rows can't surface duplicates either. Pure-shared, no Electron / IPC dependencies — runs identically in the renderer for preview and in the main process for IPC. Every clock shape — bare, compact (805p), sloppy (8.05p / 8;05pm), and both ends of a range (8-9p) — resolves through the one shared clock primitive parseClockToken exported from src/shared/snooze-time-parser.ts (the same primitive snooze and alarms use); stripTimeRange extracts the range and feeds the end clock to computeEndDate.
  • Time-zone honoring: the parser's extractTimezone (in calendar-quick-add-parser.ts) captures a written zone — replacing the old stripTimezone that discarded it — and stampZone tags every interpretation with the resolved IANA zone. The renderer's buildCreateEventInput converts the wall-clock to an offset-bearing instant for that zone via the shared, DST-correct wall-clock-zone.ts (hostIsoToZonedInstantIso + zoneAbbreviation for the preview label) and sends { dateTime, timeZone }, so the strict z.string().datetime({ offset: true }) create schema is untouched. No zone typed → host zone (byte-identical to prior behavior). Abbreviation→IANA covers the US zones + UTC/GMT; anything unrecognized falls through to the AI fallback. Locked by wall-clock-zone.test.ts (summer + winter offsets) and the zone block in calendar-quick-add-wide-hardening.test.ts — contract a-typed-timezone-is-captured-and-honored.
  • Calendar picker: pure helper quick-launch-calendar-picker.ts (writableCalendars / resolveInitialCalendarId / calendarPickerLabel, isWritableCalendar fail-open) filters the calendar list to writable calendars — CalendarInfo + listCalendars now carry accessRole. The tab renders a <Select> only when ≥2 writable calendars exist, seeds it from the saved calendarQuickAddDefaultCalendarId (falling back to primary when the saved calendar is gone), and persists a change through the canonical persistSetting() fire-and-forget path. 'primary' maps to undefined on submit so an unchanged default produces a byte-identical create payload. Contract the-user-picks-the-calendar-and-the-choice-sticks.
  • LLM fallback (Haiku via OpenRouter): src/main/services/google/calendar-ai-service.ts quickAddParse() — runs ONLY when the regex parser returns zero rows AND the $0.10/day cap is not exhausted. Emits CALENDAR_PARSE_LLM_STARTED push immediately before the Haiku call so the renderer can flip on the "AI is thinking…" indicator. JSON-mode routed through OpenRouter (Anthropic-direct silently drops response_format). Schema-validated via Zod, circuit-breaker-wrapped, every call writes source: 'calendar-quick-add' to api_cost_log so the cap query (getTodayCostByLabel) sees it tomorrow.
  • IPC:
    • CALENDAR_PARSE_QUICK_ADD — input { item: string } (the repeat?: string field was dropped 2026-05-24 along with the Repeat UI input), output { interpretations: QuickAddInterpretation[], parseSource: 'regex' | 'llm' | 'cap-exhausted' | 'llm-failed' }. Zod-validated via wrapHandler().
    • CALENDAR_PARSE_LLM_STARTED (push, main → renderer) — empty-payload fire-and-forget signal that a Haiku call is in flight. Drives the AI-thinking indicator. Registered in CRITICAL_PUSH_CHANNELS + CRITICAL_SCHEMA_MAP for the strict-payload check and exempted from the mobile-push-listener-coverage lint as a desktop-only overlay (Quick Launch isn't a mobile surface).
    • QUICK_LAUNCH_SET_WINDOW_HEIGHT — input { height: number }, clamped to [420, 720] in main and re-centered around the primary display. The renderer uses a ResizeObserver on the preview list's scrollHeight to grow the window when the interpretation rows would clip; it snaps back to 420 on success / cancel / unmount and in the empty state (the observer only runs once there are interpretations, so the idle window stays at its compact default). The preview list stays content-sized (never flex-grow) so its measured height can't feed back into the window it drives — invariant I35 (fixes the 2026-07-07 giant-empty-box regression). Best-effort — failures fall back to internal scroll within the default-size window.
    • CALENDAR_CREATE_EVENT — existing handler used unchanged. Calendar Quick Add adds no new backend write paths; it front-ends the same createEvent() that the Calendar AI chat already uses.
    • CALENDAR_DELETE_EVENT — used by the toast's Undo link (now in the main window). Existing handler.
    • CALENDAR_LIST_CALENDARS — existing handler; its output now includes accessRole per calendar so the picker can offer only writable ones.
    • SETTINGS_GET / SETTINGS_UPDATE — the picker reads the saved default calendar on mount and persists a change (via persistSetting()); no new channels, reused from the shared settings path.
    • QUICK_LAUNCH_CALENDAR_NOTIFY_SUCCESS — invoke (QL → main): relays a successful create so main can re-emit the toast push. CALENDAR_QUICK_ADD_CONFIRMED — push (main → main window): the main-window listener fires the confirmation toast. Both carry { summary, htmlLink?, eventId, calendarId? } (one shape).
  • Daily $ cap: enforced by src/main/services/api-cost-tracker.ts getTodayCostByLabel('calendar-quick-add'). Constant QUICK_ADD_DAILY_USD_CAP = 0.10 at the top of calendar-ai-service.ts. Pre-flight gate fires before the OpenRouter call; mid-flight cost-rollup keeps the cap honest across concurrent QL opens.
  • Startup migration: one-shot backfill in src/main/index.ts after the persisted log level apply — appends 'calendar' to AppSettings.quickLaunchPinnedActionIds for existing installs, then sets the sentinel AppSettings.calendarQuickAddBackfilledAt to today's ISO timestamp so the backfill never re-runs. Idempotent and non-fatal — wrapped in a try/catch that logs and continues.
  • Voice mic: same QuickLaunchVoiceMic used by other tabs — partial transcripts append to the Event field. Inline recurrence in the transcript flows through the regex parser like any other input.
  • UI anchors: 8 entries, co-located in quick-launch-calendar-tab.ui-anchors.ts (assembled via npm run ui-anchors:reindex) — tab button, Event textarea, AI badge, preview list, first preview row, Create button, the Reconnect-Google button (quick-launch-calendar-reconnect-google) shown on an auth failure, and the Repeat checkbox (quick-launch-calendar-repeat). The former success-pane / Undo / Open-in-Google anchors were removed on 2026-06-23 when the success pane became a main-window toast. Required for App Tour spotlights and the GET /ui/snapshot CLI route.

Related

The user-facing walkthrough of the tab is on the parent page. The composer it lives in is the Quick Launch modal, the connection that gates it is on the Google Integrations page, and the alarm tab that shares the same natural-language, regex-first, Haiku-fallback shape is on the Alarms page. The date and time grammar is the same engine as the one on the Snooze a session page.

Last verified 2026-10-06