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
This commit is contained in:
gagan 2026-07-02 00:35:35 +05:30 committed by GitHub
parent 932a10c2f9
commit 08a4077d43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1187 additions and 11 deletions

View file

@ -1248,6 +1248,47 @@ const ipcSchemas = {
notes: z.string(),
}),
},
// Resolve a meeting's attendees against the knowledge base — returns each
// attendee's existing person note (or null). Deterministic, no LLM; powers
// the ambient "Next up" prep card.
'meeting-prep:resolve': {
req: z.object({
attendees: z.array(z.object({
email: z.string().optional(),
displayName: z.string().optional(),
self: z.boolean().optional(),
})),
// When provided, the response includes any pre-generated prep note for
// this calendar event (matched by the eventId stamped in frontmatter).
eventId: z.string().optional(),
}),
res: z.object({
attendees: z.array(z.object({
label: z.string(),
email: z.string().optional(),
displayName: z.string().optional(),
note: z.object({
path: z.string(),
name: z.string(),
role: z.string().optional(),
organization: z.string().optional(),
markdown: z.string(),
}).nullable(),
})),
organizations: z.array(z.object({
path: z.string(),
name: z.string(),
markdown: z.string(),
})),
// The pre-generated prep note (brief + path), if one exists for eventId.
prepNote: z.object({
path: z.string(),
brief: z.string(),
}).nullable(),
matchedCount: z.number().int().nonnegative(),
unmatchedCount: z.number().int().nonnegative(),
}),
},
// Inline task schedule classification
'export:note': {
req: z.object({