@agent-native/core 0.159.2 → 0.159.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +11 -6
- package/corpus/templates/clips/AGENTS.md +1 -0
- package/corpus/templates/clips/actions/lib/meeting-content.ts +15 -4
- package/corpus/templates/clips/actions/list-meetings.ts +179 -42
- package/corpus/templates/clips/actions/search-meetings.ts +277 -0
- package/corpus/templates/clips/app/components/meetings/agenda-card.tsx +273 -0
- package/corpus/templates/clips/app/components/meetings/day-grouped-card.tsx +113 -0
- package/corpus/templates/clips/app/components/meetings/meeting-history-row.tsx +117 -0
- package/corpus/templates/clips/app/hooks/use-navigation-state.ts +12 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +7 -2
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +263 -357
- package/corpus/templates/clips/changelog/2026-08-14-meetings-history-is-searchable-again.md +6 -0
- package/corpus/templates/clips/desktop/design-refs/granola-ux.md +17 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +29 -2
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +11 -11
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -50
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +57 -20
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +30 -0
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +204 -51
- package/corpus/templates/content/app/global.css +4 -1
- package/corpus/templates/content/app/i18n-data.ts +30 -0
- package/corpus/templates/content/changelog/2026-08-12-toggle-blocks-now-follow-notion-style-enter-and-shift-tab-be.md +6 -0
- package/corpus/templates/content/docs/solutions/2026-08-12-toggle-summary-focus-persistence-shape.md +733 -0
- package/corpus/templates/content/shared/builder-mdx.ts +44 -4
- package/corpus/templates/slides/actions/get-layout-overflows.ts +65 -2
- package/dist/deploy/build.d.ts +6 -4
- package/dist/deploy/build.js +27 -11
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +2 -2
- package/dist/secrets/routes.d.ts +6 -6
- package/package.json +2 -2
- package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +0 -333
package/corpus/README.md
CHANGED
|
@@ -25,9 +25,9 @@ For press-and-hold dictations (Hold-Fn / Cmd+Shift+Space) and the `/dictate` tab
|
|
|
25
25
|
|
|
26
26
|
## Design reference
|
|
27
27
|
|
|
28
|
-
The Meetings tab intentionally mirrors **Granola**: card
|
|
28
|
+
The Meetings tab intentionally mirrors **Granola**: a compact "Coming up" card over a dense reverse-chronological history, two-pane detail (transcript left + AI notes right), inline title edit, "Generate notes" button, per-attendee action items. See `templates/clips/desktop/design-refs/granola-ux.md` for the source-of-truth interaction notes — read this before redesigning any Meetings surface. (Wispr-style press-and-hold patterns belong in the Dictate skill, not here.)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
The list was a grid of tiles until 2026-08-14. That contradicted §2 of the ref ("list-row style rather than tile cards") and cost so much vertical space that history sat below the fold, which is how a user ends up unable to find last month's call.
|
|
31
31
|
|
|
32
32
|
## Data model touched
|
|
33
33
|
|
|
@@ -75,7 +75,8 @@ When meetings are enabled, the Clips desktop app also watches for native Zoom (`
|
|
|
75
75
|
|
|
76
76
|
| Action | What it does |
|
|
77
77
|
| ------------------------- | --------------------------------------------------------------------- |
|
|
78
|
-
| `list-meetings` | Upcoming + past, scoped via `accessFilter`; reads connected Google Calendar live |
|
|
78
|
+
| `list-meetings` | Upcoming + past, scoped via `accessFilter`; reads connected Google Calendar live. `hasContent` filters to meetings worth reopening; `offset` + `hasMore` page the history |
|
|
79
|
+
| `search-meetings` | Find a meeting by title, summary, notes, attendee, or linked transcript text, with a match snippet. Use this — not `list-meetings` — when the user describes what was said rather than when it happened |
|
|
79
80
|
| `get-meeting` | One meeting + participants + segments + notes |
|
|
80
81
|
| `create-meeting` | Create a meeting row (`source`: `calendar` / `adhoc` / `manual`); desktop adhoc Zoom/Teams detection passes `source: "adhoc"` |
|
|
81
82
|
| `update-meeting` | Inline title/notes edits and owner/admin visibility changes; meeting share links include the full transcript whenever one exists |
|
|
@@ -159,20 +160,24 @@ When on `view: "dictate"`, the block instead contains a `dictation` object with
|
|
|
159
160
|
| "Summarize the standup I just finished" | `pnpm action finalize-meeting --id=<id>` (delegates to agent for Gemini cleanup) |
|
|
160
161
|
| "Create a meeting note for the call I just finished" | Prefer the current calendar event. If it was not on the calendar, send the user to `/record` instead of creating a fake meeting from the UI. |
|
|
161
162
|
| "Connect my Google Calendar" | `pnpm action connect-calendar --provider=google` then open returned `authUrl` |
|
|
162
|
-
| "Show my action items from last week" | `list-meetings --
|
|
163
|
+
| "Show my action items from last week" | `list-meetings --view=past --hasContent`, then collect `actionItemsJson` and filter by `assigneeEmail` |
|
|
163
164
|
|
|
164
165
|
## How the agent uses Meetings
|
|
165
166
|
|
|
166
167
|
These flows are common enough to memorize:
|
|
167
168
|
|
|
168
|
-
- **"Summarize my last meeting with Alice"** — `
|
|
169
|
+
- **"Summarize my last meeting with Alice"** — `search-meetings --query=alice`, pick the most recent, `get-meeting`, then `finalize-meeting` if `summaryMd` is empty.
|
|
170
|
+
- **"Find the call where we discussed the renewal"** — `search-meetings --query="renewal"`. It reads transcripts, so it finds calls the user never titled usefully; `list-meetings` cannot answer this.
|
|
169
171
|
- **"Show me action items I owe Bob"** — `list-meetings` (recent), aggregate `actionItemsJson`, filter `assigneeEmail` matching Bob's email. Mention the mic+system caveat if the user expects coverage of remote attendees.
|
|
170
172
|
- **"Create a meeting note for the call I just finished"** — prefer an existing calendar-synced meeting. If the call was not on the calendar, send the user to `/record`; do not invent a fake calendar meeting in the visible Meetings list.
|
|
171
173
|
- **"What did Alice commit to in last Tuesday's standup?"** — `get-meeting`, scan `actionItemsJson` filtered by assignee, fall back to grepping the transcript segments tagged `source: "system"` (since Alice is remote).
|
|
172
174
|
|
|
173
175
|
## UI conventions (don't break)
|
|
174
176
|
|
|
175
|
-
- **
|
|
177
|
+
- **Upcoming is one compact card** ("Coming up"), never a tile grid: a date column per day, one line per event, and Join / Open notes only on the live or imminent row. It must not push history below the fold.
|
|
178
|
+
- **History is dense one-line rows**, grouped by day with a date header (Today / Yesterday / Weekday Date): attendee avatars, title, attendee names, right-aligned time. No summary preview and no per-row status pills — transcript/notes state belongs on the detail page, not repeated down a list.
|
|
179
|
+
- **History is paged, never capped.** The list reads `list-meetings` with `hasContent: true` (not `recordedOnly`) so desktop live notes without a linked recording still appear, and pages with `offset` + `hasMore` behind a "Load older" button.
|
|
180
|
+
- **The search box is server-side**, calling `search-meetings`. Never filter the loaded page client-side: the meeting a user is hunting for is usually one they have not scrolled to.
|
|
176
181
|
- **Calendar-sourced list**: no "New meeting" CTA and no manual sync requirement in the Meetings list. Users connect/reconnect/disconnect the calendar from the calendar settings menu; events are fetched live from Google Calendar.
|
|
177
182
|
- **Two-pane detail**: transcript (left) + AI notes (right) with a "Generate notes" button in the header.
|
|
178
183
|
- **Live indicator** is a red animated dot — never a sparkle or a robot icon.
|
|
@@ -88,6 +88,7 @@ video sharing app. The agent and the UI share the same SQL data and actions.
|
|
|
88
88
|
| `regenerate-title`, `-summary`, `-chapters` | AI metadata |
|
|
89
89
|
| `trim-`, `split-recording`, `remove-silences`, `remove-filler-words` | Edits |
|
|
90
90
|
| `list-meetings`, `get-`, `update-`, `finalize-meeting` | Meetings |
|
|
91
|
+
| `search-meetings` | Find a meeting by title, summary, notes, attendee, or transcript |
|
|
91
92
|
| `list-dictations`, `cleanup-dictation` | Dictation history |
|
|
92
93
|
| `add-comment`, `update-comment`, `create-folder`, `create-space` | Comments, folders |
|
|
93
94
|
| `share-resource`, `set-resource-visibility`, `build-embed-url` | Share, embed |
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* What makes a meeting worth showing in history.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A calendar-sourced `clips_meetings` row is created as soon as the user
|
|
5
|
+
* records or edits an event, so the table also holds bare husks that carry
|
|
6
|
+
* nothing a user would ever reopen. Two places need that distinction and they
|
|
7
|
+
* MUST agree:
|
|
8
|
+
*
|
|
9
|
+
* 1. this predicate, for rows already in memory, and
|
|
10
|
+
* 2. `meetingHasContentFilter()` in `../list-meetings.ts`, its SQL mirror.
|
|
11
|
+
*
|
|
12
|
+
* The same rule decides which rows the history list returns AND which
|
|
13
|
+
* persisted rows a live calendar event may supersede. Let the two drift and
|
|
14
|
+
* history rows start vanishing behind their own calendar events — which is the
|
|
15
|
+
* bug that made `recordedOnly` look like a reasonable filter in the first
|
|
16
|
+
* place. `recordingId` alone is NOT the rule: desktop live notes produce a
|
|
17
|
+
* meeting with a summary and no linked recording.
|
|
7
18
|
*/
|
|
8
19
|
export interface MeetingContentFields {
|
|
9
20
|
recordingId?: string | null;
|
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
* List meetings visible to the current user.
|
|
3
3
|
*
|
|
4
4
|
* Filtering:
|
|
5
|
-
* - view='upcoming' — scheduled_start in the future
|
|
6
|
-
* - view='
|
|
5
|
+
* - view='upcoming' — scheduled_start in the future and not yet started
|
|
6
|
+
* - view='agenda' — scheduled_start from `agendaLookbackMin` ago onward,
|
|
7
|
+
* started or not. The Meetings tab's rolling day view.
|
|
8
|
+
* - view='past' — actual_end OR scheduled_end in the past, OR a
|
|
9
|
+
* manual/ad-hoc meeting with no scheduling and no
|
|
10
|
+
* in-progress recording; not trashed
|
|
7
11
|
* - view='all' — every visible meeting (excluding trashed)
|
|
8
12
|
* - view='trash' — trashed_at is not null
|
|
9
13
|
*
|
|
14
|
+
* 'upcoming' and 'agenda' differ on purpose: desktop reminders need "has not
|
|
15
|
+
* started yet", the Meetings agenda needs "belongs to the day you are in".
|
|
16
|
+
*
|
|
17
|
+
* `hasContent` narrows any view to meetings that actually hold something —
|
|
18
|
+
* see `./lib/meeting-content.ts`. The Meetings history list uses it so notes
|
|
19
|
+
* taken without a linked recording still appear.
|
|
20
|
+
*
|
|
10
21
|
* Calendar behavior:
|
|
11
22
|
* Connected Google Calendar accounts are read live on every call. We only
|
|
12
23
|
* materialize a calendar event into `clips_meetings` when the user records
|
|
@@ -21,6 +32,7 @@ import {
|
|
|
21
32
|
asc,
|
|
22
33
|
desc,
|
|
23
34
|
eq,
|
|
35
|
+
inArray,
|
|
24
36
|
isNull,
|
|
25
37
|
isNotNull,
|
|
26
38
|
lt,
|
|
@@ -52,8 +64,14 @@ import { booleanParam } from "./lib/cli-params.js";
|
|
|
52
64
|
import { meetingRowHasContent } from "./lib/meeting-content.js";
|
|
53
65
|
|
|
54
66
|
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
67
|
+
// Must stay above `limit`'s own max (500): the persisted query always fetches
|
|
68
|
+
// one row past the caller's limit as a `hasMore` probe, so if this cap ever
|
|
69
|
+
// equalled 500 a caller requesting the max limit would have that probe row
|
|
70
|
+
// truncated away and `hasMore` would silently read false forever at that
|
|
71
|
+
// exact boundary, even with more rows still to page through.
|
|
72
|
+
const MAX_PERSISTED_ROWS_PER_QUERY = 1000;
|
|
55
73
|
|
|
56
|
-
/** SQL mirror of `meetingRowHasContent`. Keep
|
|
74
|
+
/** SQL mirror of `meetingRowHasContent`. Keep the two in lockstep. */
|
|
57
75
|
function meetingHasContentFilter() {
|
|
58
76
|
return or(
|
|
59
77
|
isNotNull(schema.meetings.recordingId),
|
|
@@ -71,9 +89,20 @@ export default defineAction({
|
|
|
71
89
|
"List meetings (Granola-style) the current user has access to. Connected calendars are read live; use view='upcoming' / 'past' / 'all' / 'trash' to filter by lifecycle.",
|
|
72
90
|
schema: z.object({
|
|
73
91
|
view: z
|
|
74
|
-
.enum(["upcoming", "past", "all", "trash"])
|
|
92
|
+
.enum(["upcoming", "agenda", "past", "all", "trash"])
|
|
75
93
|
.default("upcoming")
|
|
76
|
-
.describe(
|
|
94
|
+
.describe(
|
|
95
|
+
"Which list to show. 'agenda' is the Meetings tab's rolling window — everything scheduled from `agendaLookbackMin` ago onward, so calls that already happened today stay on the agenda; 'upcoming' is strictly not-yet-started and is what desktop reminders poll.",
|
|
96
|
+
),
|
|
97
|
+
agendaLookbackMin: z.coerce
|
|
98
|
+
.number()
|
|
99
|
+
.int()
|
|
100
|
+
.min(0)
|
|
101
|
+
.max(60 * 24 * 7)
|
|
102
|
+
.default(60 * 24)
|
|
103
|
+
.describe(
|
|
104
|
+
"How far back view='agenda' reaches, in minutes. Default 1440 (24h): a meeting from earlier today is still part of today.",
|
|
105
|
+
),
|
|
77
106
|
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
78
107
|
offset: z.coerce.number().int().min(0).default(0),
|
|
79
108
|
recordedOnly: booleanParam
|
|
@@ -82,7 +111,7 @@ export default defineAction({
|
|
|
82
111
|
hasContent: booleanParam
|
|
83
112
|
.default(false)
|
|
84
113
|
.describe(
|
|
85
|
-
"Only return persisted meetings that
|
|
114
|
+
"Only return persisted meetings that hold something worth reopening — a linked recording, an actual start/end, notes, a summary, bullets, or action items. Prefer this over recordedOnly for history: live notes taken without a linked recording still count.",
|
|
86
115
|
),
|
|
87
116
|
includeLiveCalendar: booleanParam
|
|
88
117
|
.default(true)
|
|
@@ -129,9 +158,11 @@ export default defineAction({
|
|
|
129
158
|
// slice(offset, offset + limit) at the end. To make that final slice
|
|
130
159
|
// correct we must fetch enough rows from BOTH sources to cover the whole
|
|
131
160
|
// offset + limit window before merging — fetching only `limit` would drop
|
|
132
|
-
// events once offset > 0 or the calendar is large. Keep the
|
|
133
|
-
// so a huge calendar
|
|
134
|
-
// the
|
|
161
|
+
// events once offset > 0 or the calendar is large. Keep the hard caps
|
|
162
|
+
// (MAX_PERSISTED_ROWS_PER_QUERY persisted, 250 live) so a huge calendar
|
|
163
|
+
// can't blow up the request. We fetch one row past the window purely as a
|
|
164
|
+
// `hasMore` probe: the extra row is never returned, it only tells the
|
|
165
|
+
// caller another page exists.
|
|
135
166
|
const windowCount = args.offset + args.limit;
|
|
136
167
|
const upcomingWindowMaxIso = args.upcomingWithinMin
|
|
137
168
|
? new Date(
|
|
@@ -143,6 +174,28 @@ export default defineAction({
|
|
|
143
174
|
startedWithinMin > 0
|
|
144
175
|
? new Date(now.getTime() - startedWithinMin * 60 * 1000).toISOString()
|
|
145
176
|
: nowIso;
|
|
177
|
+
const agendaFloorIso = new Date(
|
|
178
|
+
now.getTime() - args.agendaLookbackMin * 60 * 1000,
|
|
179
|
+
).toISOString();
|
|
180
|
+
// Mirrors the live calendar's own forward cap (see timeMax below) so a
|
|
181
|
+
// meeting scheduled months out can't sit ahead of nearer ones in a
|
|
182
|
+
// consistent window; harmless under the ascending sort today, but keeps
|
|
183
|
+
// "agenda" honestly meaning "the near future" if that ever changes.
|
|
184
|
+
const agendaCeilingIso = new Date(
|
|
185
|
+
now.getTime() + THIRTY_DAYS_MS,
|
|
186
|
+
).toISOString();
|
|
187
|
+
// 'agenda' and 'upcoming' both read forward in time, so they share the
|
|
188
|
+
// ascending sort. They differ in where the window starts and in whether a
|
|
189
|
+
// meeting that already started is still allowed in.
|
|
190
|
+
const isForwardLooking = args.view === "upcoming" || args.view === "agenda";
|
|
191
|
+
// Live calendar events require a global re-sort against persisted rows, so
|
|
192
|
+
// that branch keeps the "fetch the whole window from offset 0" approach.
|
|
193
|
+
// Every other view can paginate for real in SQL — which matters here
|
|
194
|
+
// because the whole-window approach caps out at 500 rows no matter how
|
|
195
|
+
// large `offset` grows, silently stranding "Load older" once history
|
|
196
|
+
// passes 500 meetings.
|
|
197
|
+
const willMergeLiveCalendar =
|
|
198
|
+
args.includeLiveCalendar && !args.recordedOnly && args.view !== "trash";
|
|
146
199
|
|
|
147
200
|
const whereClauses = [accessFilter(schema.meetings, schema.meetingShares)];
|
|
148
201
|
|
|
@@ -166,14 +219,41 @@ export default defineAction({
|
|
|
166
219
|
: undefined,
|
|
167
220
|
)!,
|
|
168
221
|
);
|
|
222
|
+
} else if (args.view === "agenda") {
|
|
223
|
+
// Everything scheduled from the lookback floor onward. Deliberately does
|
|
224
|
+
// NOT exclude meetings that already started or ended: "the call you just
|
|
225
|
+
// finished" is the most useful row on a day's agenda, and excluding it is
|
|
226
|
+
// what made the old upcoming-only list feel like it had lost your day.
|
|
227
|
+
whereClauses.push(
|
|
228
|
+
and(
|
|
229
|
+
isNotNull(schema.meetings.scheduledStart),
|
|
230
|
+
gte(schema.meetings.scheduledStart, agendaFloorIso),
|
|
231
|
+
lte(schema.meetings.scheduledStart, agendaCeilingIso),
|
|
232
|
+
)!,
|
|
233
|
+
);
|
|
169
234
|
} else if (args.view === "past") {
|
|
170
|
-
// Either completed (actualEnd set)
|
|
235
|
+
// Either completed (actualEnd set), scheduled-end in the past, or a
|
|
236
|
+
// manual/ad-hoc meeting with no scheduling and no in-progress recording
|
|
237
|
+
// at all. That last group (dictation-style notes with no calendar event
|
|
238
|
+
// and no actualStart) can never satisfy 'agenda' or 'upcoming' — both
|
|
239
|
+
// require scheduledStart — so 'past' is the only lifecycle view left
|
|
240
|
+
// for their content to surface in. A meeting that IS actively recording
|
|
241
|
+
// (actualStart set, actualEnd not yet) still waits for actualEnd.
|
|
171
242
|
whereClauses.push(
|
|
172
243
|
or(
|
|
173
244
|
isNotNull(schema.meetings.actualEnd),
|
|
174
245
|
and(
|
|
175
246
|
isNotNull(schema.meetings.scheduledEnd),
|
|
176
247
|
lt(schema.meetings.scheduledEnd, nowIso),
|
|
248
|
+
// A meeting that started recording and is still going (actualStart
|
|
249
|
+
// set, actualEnd not yet) waits for actualEnd even if its schedule
|
|
250
|
+
// says it should be over — otherwise it double-appears here and on
|
|
251
|
+
// the Agenda while still in progress.
|
|
252
|
+
isNull(schema.meetings.actualStart),
|
|
253
|
+
)!,
|
|
254
|
+
and(
|
|
255
|
+
isNull(schema.meetings.scheduledStart),
|
|
256
|
+
isNull(schema.meetings.actualStart),
|
|
177
257
|
)!,
|
|
178
258
|
)!,
|
|
179
259
|
);
|
|
@@ -185,22 +265,55 @@ export default defineAction({
|
|
|
185
265
|
whereClauses.push(meetingHasContentFilter());
|
|
186
266
|
}
|
|
187
267
|
|
|
188
|
-
const orderBy =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
268
|
+
const orderBy = isForwardLooking
|
|
269
|
+
? [asc(schema.meetings.scheduledStart)]
|
|
270
|
+
: [
|
|
271
|
+
desc(
|
|
272
|
+
sql`COALESCE(${schema.meetings.actualStart}, ${schema.meetings.scheduledStart}, ${schema.meetings.createdAt})`,
|
|
273
|
+
),
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
// `persistedHasMore` is only meaningful (and only trusted below) on the
|
|
277
|
+
// no-merge path — the merge path derives its own `hasMore` from the
|
|
278
|
+
// combined, re-sorted array once live events are folded in.
|
|
279
|
+
let persistedHasMore = false;
|
|
280
|
+
let rows: Array<typeof schema.meetings.$inferSelect>;
|
|
281
|
+
if (willMergeLiveCalendar) {
|
|
282
|
+
rows = await db
|
|
283
|
+
.select()
|
|
284
|
+
.from(schema.meetings)
|
|
285
|
+
.where(and(...whereClauses))
|
|
286
|
+
.orderBy(...orderBy)
|
|
287
|
+
.limit(Math.min(MAX_PERSISTED_ROWS_PER_QUERY, windowCount + 1))
|
|
288
|
+
.offset(0);
|
|
289
|
+
} else {
|
|
290
|
+
const page = await db
|
|
291
|
+
.select()
|
|
292
|
+
.from(schema.meetings)
|
|
293
|
+
.where(and(...whereClauses))
|
|
294
|
+
.orderBy(...orderBy)
|
|
295
|
+
.limit(Math.min(MAX_PERSISTED_ROWS_PER_QUERY, args.limit + 1))
|
|
296
|
+
.offset(args.offset);
|
|
297
|
+
persistedHasMore = page.length > args.limit;
|
|
298
|
+
rows = page.slice(0, args.limit);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Participants drive the history row's avatar stack and "who was on this
|
|
302
|
+
// call" subtitle. Live calendar events carry their own attendees, so this
|
|
303
|
+
// only backfills persisted rows, in one batched read rather than per row.
|
|
304
|
+
const persistedIds = rows.map((m) => m.id);
|
|
305
|
+
const participantRows = persistedIds.length
|
|
306
|
+
? await db
|
|
307
|
+
.select()
|
|
308
|
+
.from(schema.meetingParticipants)
|
|
309
|
+
.where(inArray(schema.meetingParticipants.meetingId, persistedIds))
|
|
310
|
+
: [];
|
|
311
|
+
const participantsByMeeting = new Map<string, typeof participantRows>();
|
|
312
|
+
for (const participant of participantRows) {
|
|
313
|
+
const list = participantsByMeeting.get(participant.meetingId) ?? [];
|
|
314
|
+
list.push(participant);
|
|
315
|
+
participantsByMeeting.set(participant.meetingId, list);
|
|
316
|
+
}
|
|
204
317
|
|
|
205
318
|
// Add a derived `summaryPreview` (first ~100 chars of summaryMd) so the
|
|
206
319
|
// Granola-style cards can render a one-liner without re-parsing markdown.
|
|
@@ -209,7 +322,11 @@ export default defineAction({
|
|
|
209
322
|
const preview = summary
|
|
210
323
|
? summary.replace(/\s+/g, " ").slice(0, 100)
|
|
211
324
|
: null;
|
|
212
|
-
return {
|
|
325
|
+
return {
|
|
326
|
+
...m,
|
|
327
|
+
summaryPreview: preview,
|
|
328
|
+
participants: participantsByMeeting.get(m.id) ?? [],
|
|
329
|
+
};
|
|
213
330
|
});
|
|
214
331
|
|
|
215
332
|
const liveMeetings: any[] = [];
|
|
@@ -231,11 +348,7 @@ export default defineAction({
|
|
|
231
348
|
// Google event externalId so we can match it against the emitted set.
|
|
232
349
|
const calendarEventIdToExternalId = new Map<string, string>();
|
|
233
350
|
|
|
234
|
-
if (
|
|
235
|
-
args.includeLiveCalendar &&
|
|
236
|
-
!args.recordedOnly &&
|
|
237
|
-
args.view !== "trash"
|
|
238
|
-
) {
|
|
351
|
+
if (willMergeLiveCalendar) {
|
|
239
352
|
const accountWhere = [
|
|
240
353
|
accessFilter(schema.calendarAccounts, schema.calendarAccountShares),
|
|
241
354
|
eq(schema.calendarAccounts.status, "connected"),
|
|
@@ -269,10 +382,12 @@ export default defineAction({
|
|
|
269
382
|
? new Date(now.getTime() - THIRTY_DAYS_MS).toISOString()
|
|
270
383
|
: args.view === "all"
|
|
271
384
|
? new Date(now.getTime() - THIRTY_DAYS_MS).toISOString()
|
|
272
|
-
:
|
|
273
|
-
?
|
|
274
|
-
:
|
|
275
|
-
|
|
385
|
+
: args.view === "agenda"
|
|
386
|
+
? agendaFloorIso
|
|
387
|
+
: startedWithinMin > 0
|
|
388
|
+
? upcomingWindowMinIso
|
|
389
|
+
: // Small cushion for clock skew when listing pure upcoming.
|
|
390
|
+
new Date(now.getTime() - 60 * 1000).toISOString();
|
|
276
391
|
const timeMax =
|
|
277
392
|
args.view === "past"
|
|
278
393
|
? nowIso
|
|
@@ -348,6 +463,14 @@ export default defineAction({
|
|
|
348
463
|
continue;
|
|
349
464
|
}
|
|
350
465
|
if (args.view === "past" && endMs >= now.getTime()) continue;
|
|
466
|
+
// The agenda keeps already-finished events, but only back to its
|
|
467
|
+
// floor — anything that started before it belongs in Past.
|
|
468
|
+
if (
|
|
469
|
+
args.view === "agenda" &&
|
|
470
|
+
startMs < Date.parse(agendaFloorIso)
|
|
471
|
+
) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
351
474
|
if (
|
|
352
475
|
upcomingWindowMaxIso &&
|
|
353
476
|
startMs > Date.parse(upcomingWindowMaxIso)
|
|
@@ -364,11 +487,16 @@ export default defineAction({
|
|
|
364
487
|
meeting: persisted,
|
|
365
488
|
});
|
|
366
489
|
if (liveMeeting) {
|
|
367
|
-
|
|
490
|
+
// Mark the event as emitted regardless of hasContent, so an
|
|
491
|
+
// empty correlated persisted husk (below) stays suppressed
|
|
492
|
+
// rather than reappearing once its live event is filtered out.
|
|
368
493
|
emittedLiveEventKeys.add(liveMeeting.id);
|
|
369
494
|
if (liveMeeting.calendarExternalId) {
|
|
370
495
|
emittedLiveEventKeys.add(liveMeeting.calendarExternalId);
|
|
371
496
|
}
|
|
497
|
+
if (!args.hasContent || meetingRowHasContent(liveMeeting)) {
|
|
498
|
+
liveMeetings.push(liveMeeting);
|
|
499
|
+
}
|
|
372
500
|
}
|
|
373
501
|
}
|
|
374
502
|
|
|
@@ -415,6 +543,18 @@ export default defineAction({
|
|
|
415
543
|
combined.push(meeting);
|
|
416
544
|
}
|
|
417
545
|
|
|
546
|
+
// Without a live merge, `combined` is exactly `persistedMeetings` — a page
|
|
547
|
+
// the DB already ordered and offset correctly. Re-sorting by a different
|
|
548
|
+
// key and re-slicing by `offset` here would both scramble that order and
|
|
549
|
+
// apply the offset a second time, so the no-merge path returns as-is.
|
|
550
|
+
if (!willMergeLiveCalendar) {
|
|
551
|
+
return {
|
|
552
|
+
meetings: combined,
|
|
553
|
+
calendarErrors,
|
|
554
|
+
hasMore: persistedHasMore,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
418
558
|
combined.sort((a, b) => {
|
|
419
559
|
const aStart = Date.parse(a.scheduledStart ?? a.createdAt ?? "");
|
|
420
560
|
const bStart = Date.parse(b.scheduledStart ?? b.createdAt ?? "");
|
|
@@ -424,11 +564,8 @@ export default defineAction({
|
|
|
424
564
|
});
|
|
425
565
|
|
|
426
566
|
const meetings = combined.slice(args.offset, args.offset + args.limit);
|
|
567
|
+
const hasMore = combined.length > args.offset + args.limit;
|
|
427
568
|
|
|
428
|
-
return {
|
|
429
|
-
meetings,
|
|
430
|
-
calendarErrors,
|
|
431
|
-
hasMore: combined.length > args.offset + args.limit,
|
|
432
|
-
};
|
|
569
|
+
return { meetings, calendarErrors, hasMore };
|
|
433
570
|
},
|
|
434
571
|
});
|