rowboat/PLAN.md
gagan 08a4077d43
Meeting prep: attendee notes + proactive 6h-ahead prep briefs (#636)
* feat(meetings): inline meeting prep under next meeting

Resolve a meeting's attendees against the knowledge base and render each
attendee's existing person.md inline beneath the next upcoming meeting in
the Meetings view. Deterministic (email-exact match, then unambiguous
name/alias), no LLM in the hot path.

- core: resolveMeetingPrep() + meeting-prep:resolve IPC
- renderer: notes-first rows with expandable person.md; unmatched attendees
  collapse into a 'no notes yet' row with a Create note action that hands
  off to the Copilot
- prep re-resolves when a People note changes

* feat(meetings): per-meeting prep toggle

Add a Prep toggle to every eligible meeting row, not just the next one.
The next meeting still auto-expands; other meetings resolve their attendee
notes lazily when their toggle is opened. All-day and solo events get no
toggle.

* feat(meetings): org matching, cached index, and field-bleed fix

#2 Matching: resolve the meeting's external companies from attendee email
domains (and matched people's Organization field), excluding the user's own
domain, and surface those org notes in a Companies section. Normalize display
names (strip '(via ...)' suffixes) before name/alias matching.

#3 Perf: meeting prep reads a cached knowledge index instead of rescanning the
whole knowledge dir on every resolve; invalidated whenever a file under
knowledge/ changes (wired to the workspace watcher).

Fix: extractField in the knowledge index consumed newlines after the label, so
an empty field (e.g. **Role:**) bled the next line's value. Restrict it to
spaces/tabs so empty fields resolve to undefined. Also strip the folder prefix
from link targets for display (Organizations/Rowboat Labs -> Rowboat Labs).

* feat(meetings): proactive prep notes generated 6h ahead

Generate a meeting prep note ~6h before each meeting and surface its brief
in the prep card.

- Generator (meeting_prep_brief.ts): assembles roster + 'last time' recap
  (extracts the prior instance's Action items) + agenda, ordered by meeting
  type (recurring -> recap first, one-off -> agenda first), then one model
  call for a 'what matters' brief. Writes knowledge/Meetings/prep/<slug>-<date>.md
  with eventId/recurringEventId frontmatter. Reuses the summarizeMeeting LLM
  path (configured model, useCase: meeting_prep).
- Scheduler (meeting_prep_scheduler.ts): calendar-aware tick (5m), generates
  prep within the 6h lead window, state file dedupes, self-heals on changes.
- Card: shows the brief (bulleted, compact) above a People list whose rows
  link out to each person's note instead of rendering it inline. Generated
  prep notes are kept out of the past-notes table.

* chore(meetings): poll prep scheduler every 15m instead of 5m
2026-07-02 00:35:35 +05:30

5.9 KiB
Raw Blame History

Meeting Prep — "Next up" prep card

Goal

When a meeting is imminent, surface who will be there by pulling up each attendee's existing person.md from the knowledge base and showing it — no click, no LLM in the hot path. An ambient card that's just ready when you look.

This is a deliberately light, deterministic surface. It does not replace the two existing meeting-prep paths:

  • the chat skill (assistant/skills/meeting-prep/) — LLM brief on demand
  • the pre-built automation (pre_built/meeting-prep.md) — LLM brief files

It complements them with an instant, no-LLM "who's in this meeting" view.

Decisions (settled)

Axis Choice
Surface Ambient card, no notification. Pinned at top of Meetings view.
Activation Next eligible meeting is ≤ 30 min away. Outside that → no card.
Depth Just show person.md — render the note, no LLM synthesis.
Who Everyone incl. teammates. Skip self / all-day / declined / cancelled.
Sparse meetings Notes-first: attendees with notes shown prominently; the rest collapse into "N others — no notes yet" → expand to create.
No-note action Offer to create a note for that attendee.

Resolution rules

For each attendee (excluding self):

  1. Email exact match (case-insensitive) against KnowledgeIndex.people[].email — reliable.
  2. Else name/alias match on displayNamepeople[].name / people[].aliases, only when it's a single unambiguous hit.
  3. Matched → read person.md, return { name, role, org, path, markdown }.
  4. Unmatched → returned as a "no note" attendee (name + email).

Org notes (Organizations/{Org}.md) are deferredperson.md already carries Org/Role inline, enough for v1.

Architecture

Piece Location Notes
Attendee → note resolver new IPC meeting-prep:resolve in apps/main/src/ipc.ts Input: attendee list ({email?, displayName?, self?}[]). Builds/uses buildKnowledgeIndex(), resolves per rules above, reads matched notes, returns structured payload. Deterministic + fast.
Shared types packages/shared/src/ MeetingPrepAttendee, MeetingPrepResult request/response shapes + IPC channel typing.
"Next up" card UI new component rendered above UpcomingEvents in meetings-view.tsx Picks next event ≤30 min out, calls IPC, renders notes-first + collapsed others. Reuses existing markdown rendering.
Create-note action reuse the note-creation agent (only LLM touch, user-initiated) v1: open a prefilled Copilot chat to create the note (visible/steerable) rather than firing silently.

No new background loop — the renderer already polls calendar_sync/ every minute, so the card stays current for free.

Build slices

  1. Resolver IPC + shared typesmeeting-prep:resolve, wired through preload. Unit-testable in isolation. ← first slice
  2. "Next up" card UI — component + 30-min gating + notes-first / collapsed rendering, reading from the IPC.
  3. Create-note action — prefilled Copilot chat for an unmatched attendee.

Out of scope (fast-follow candidates)

  • Notification variant (extend notify_calendar_meetings.ts tick).
  • Org-note surfacing.
  • Configurable lead time (hardcode 30 min for v1).
  • LLM-synthesized brief button on the card.

Phase 2 — Proactive, meeting-aware prep (6h ahead)

Shift from a static contact viewer to meeting-aware prep, generated ahead of time.

Decisions

  • Trigger: a calendar-driven tick (clone of notify_calendar_meetings.ts), NOT the cron/bg-task engine — fire time is start 6h, which varies per event, so a calendar scan + state file is the right fit (self-healing on reschedule/cancel).
  • Storage: write a real note → knowledge/Meetings/prep/<slug>-<date>.md (openable/editable).
  • Notify: none — silent background generation; appears in the Meetings view.
  • Adaptive by meeting type:
    • Recurring (recurringEventId + prior notes found) → lead with "Last time" recap (prior instance's ## Action items / summary), then roster.
    • One-off / new → lead with agenda (event description), then roster + company.
  • Roster: always list every attendee; ones with a note link to it, ones without get Create note.
  • AI brief: the only LLM step. Reuses the summarizeMeeting path exactly — generateText against the user's configured model (getMeetingNotesModel / getDefaultModelAndProvider), tagged useCase: 'meeting_prep'. Deterministic parts (roster, recap, agenda) are assembled in code; the model only writes the "what matters" summary.

Build slices

  • A. Core generator (meeting_prep_brief.ts): assemble context (roster + recap + agenda, meeting-type aware) → generatePrepBrief() (one model call) → render + writePrepNote() to knowledge/Meetings/prep/. Frontmatter stamps eventId / recurringEventId / meetingDate / generatedAt.
  • B. Scheduler (meeting_prep_scheduler.ts): tick (~few min), scan calendar_sync/, for events starting within 6h, not ended, ≥1 non-self attendee, not already prepped (state file keyed by eventId) → generate + write. init() wired in main.ts beside initCalendarNotifications().
  • C. Card integration: the inline prep card prefers the generated prep note when it exists (instant), else falls back to the live deterministic resolve.

Open detail / enabler

  • Notes don't store an event id today, so recap linking is a title/date heuristic. Stamp eventId/recurringEventId into captured notes' frontmatter going forward so future links are exact.

Verification

  • Resolver: feed known attendee emails → correct note match; unknown → no-note.
  • Card: appears only inside the 30-min window; notes-first ordering; collapse toggles; create action opens prefilled chat.
  • npm run deps && npm run lint clean.