@agent-native/core 0.192.0 → 0.192.1-nightly-20260926144920

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.
Files changed (82) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/cancel-booking.ts +12 -4
  3. package/corpus/templates/calendar/actions/list-bookings.ts +23 -2
  4. package/corpus/templates/calendar/actions/list-events.ts +42 -29
  5. package/corpus/templates/calendar/actions/update-booking-link.ts +27 -1
  6. package/corpus/templates/calendar/app/hooks/use-bookings.ts +9 -2
  7. package/corpus/templates/calendar/app/i18n/en-US.ts +8 -0
  8. package/corpus/templates/calendar/app/lib/booking-sorting.ts +7 -0
  9. package/corpus/templates/calendar/app/lib/event-colors.ts +29 -4
  10. package/corpus/templates/calendar/app/pages/BookingsList.tsx +99 -19
  11. package/corpus/templates/calendar/app/pages/CalendarView.tsx +5 -11
  12. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +5 -0
  13. package/corpus/templates/calendar/changelog/2026-09-25-ambiguous-zoom-bookings-need-review.md +6 -0
  14. package/corpus/templates/calendar/changelog/2026-09-25-bookings-list-shows-newest-bookings-first.md +6 -0
  15. package/corpus/templates/calendar/changelog/2026-09-25-canceling-a-booking-now-resolves-its-zoom-meeting-first.md +6 -0
  16. package/corpus/templates/calendar/changelog/2026-09-25-colleague-events-use-sidebar-colors.md +6 -0
  17. package/corpus/templates/calendar/changelog/2026-09-25-public-booking-pages-reject-malformed-meeting-settings-befor.md +6 -0
  18. package/corpus/templates/calendar/changelog/2026-09-25-shared-booking-cancellations-require-editor-access-and-unres.md +6 -0
  19. package/corpus/templates/calendar/server/db/schema.ts +5 -0
  20. package/corpus/templates/calendar/server/handlers/availability.ts +32 -29
  21. package/corpus/templates/calendar/server/handlers/bookings.ts +189 -62
  22. package/corpus/templates/calendar/server/lib/booking-link-utils.ts +46 -1
  23. package/corpus/templates/calendar/server/lib/zoom.ts +66 -1
  24. package/corpus/templates/calendar/server/plugins/db.ts +13 -0
  25. package/corpus/templates/calendar/shared/api.ts +4 -0
  26. package/corpus/templates/clips/app/components/capture-install-options.tsx +9 -6
  27. package/corpus/templates/clips/app/components/clips-command-menu.tsx +4 -0
  28. package/corpus/templates/clips/app/i18n/en-US.ts +5 -4
  29. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +4 -0
  30. package/corpus/templates/clips/app/routes/share.$shareId.tsx +7 -33
  31. package/corpus/templates/clips/changelog/2026-09-26-desktop-platform-icon.md +6 -0
  32. package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-state-now-explains-how-screen-recordings-.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-09-26-the-empty-comments-view-has-a-clearer-headline-and-a-focused.md +6 -0
  34. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -2
  35. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +74 -40
  36. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +64 -42
  37. package/corpus/templates/design/app/components/design/design-canvas/creation.ts +1 -0
  38. package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -2
  39. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +56 -25
  40. package/corpus/templates/design/app/routes/design-systems.tsx +2 -4
  41. package/corpus/templates/design/app/routes/design.$id.tsx +2 -1
  42. package/corpus/templates/design/app/routes/public-design-meta.ts +0 -39
  43. package/corpus/templates/design/app/routes/templates.tsx +2 -4
  44. package/corpus/templates/design/app/routes/visual-edit_.$id.tsx +2 -1
  45. package/corpus/templates/design/changelog/2026-09-25-escape-finishes-open-pen-paths.md +7 -0
  46. package/corpus/templates/design/changelog/2026-09-25-pen-paths-remain-available-after-save-failures-and-retries-k.md +6 -0
  47. package/corpus/templates/design/changelog/2026-09-25-pen-retries-keep-extending-the-selected-vector-after-a-save-.md +6 -0
  48. package/corpus/templates/design/changelog/2026-09-25-pressing-enter-finishes-a-new-pen-path-and-selects-its-vector-on-move.md +6 -0
  49. package/corpus/templates/design/changelog/2026-09-26-cmd-d-places-screen-duplicates-in-the-next-open-slot.md +6 -0
  50. package/corpus/templates/design/changelog/2026-09-26-dropping-a-layer-beside-a-sibling-keeps-that-insertion-point.md +6 -0
  51. package/corpus/templates/design/changelog/2026-09-26-layers-dragged-out-of-frames-now-land-at-the-board-level-and.md +6 -0
  52. package/corpus/templates/design/changelog/2026-09-26-opening-shared-designs-no-longer-fails-during-client-startup.md +6 -0
  53. package/corpus/templates/design/package.json +1 -1
  54. package/corpus/templates/design/scripts/check-client-server-boundary.ts +55 -0
  55. package/corpus/templates/design/server/lib/public-design-meta.server.ts +40 -0
  56. package/corpus/templates/mail/app/components/email/EmailListItem.tsx +11 -9
  57. package/corpus/templates/mail/app/components/email/EmailThread.tsx +9 -1
  58. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
  59. package/corpus/templates/mail/app/components/settings/AiFilterSection.tsx +25 -2
  60. package/corpus/templates/mail/app/hooks/use-emails.ts +159 -27
  61. package/corpus/templates/mail/app/i18n/en-US.ts +2 -1
  62. package/corpus/templates/mail/app/lib/label-display.ts +6 -0
  63. package/corpus/templates/mail/app/pages/InboxPage.tsx +5 -0
  64. package/corpus/templates/mail/changelog/2026-09-26-fixed-importance-actions-label-display-and-triage-loading-fe.md +6 -0
  65. package/corpus/templates/mail/changelog/2026-09-26-mail-cancels-stale-thread-read-cooldown-retries-after-a-newe.md +6 -0
  66. package/corpus/templates/mail/server/lib/google-api.ts +12 -7
  67. package/corpus/templates/slides/app/i18n/en-US.ts +0 -1
  68. package/corpus/templates/slides/app/pages/Index.tsx +1 -29
  69. package/corpus/templates/slides/changelog/2026-09-26-create-decks-from-the-prompt-without-a-separate-new-deck-button.md +6 -0
  70. package/corpus/templates/tasks/.agents/skills/agent-watchdog/SKILL.md +7 -2
  71. package/dist/client/settings/UsageSection.js +18 -5
  72. package/dist/client/settings/recent-prompt-groups.d.ts +12 -0
  73. package/dist/client/settings/recent-prompt-groups.js +20 -0
  74. package/dist/client/use-action.js +7 -0
  75. package/dist/observability/routes.d.ts +5 -5
  76. package/dist/progress/routes.d.ts +1 -1
  77. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  78. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  79. package/dist/secrets/routes.d.ts +3 -3
  80. package/dist/server/action-routes.js +25 -2
  81. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  82. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 10136
34
+ - template files: 10160
@@ -7,16 +7,24 @@ import { requireActionUserEmail } from "./event-action-helpers.js";
7
7
 
8
8
  export default defineAction({
9
9
  description:
10
- "Cancel a booking and its linked calendar event. Use this instead of delete-event or delete-events for booking-backed events so the booking cannot be republished as confirmed.",
11
- schema: z.object({ id: z.string().min(1).describe("Booking id") }),
10
+ "Cancel a booking and its linked calendar and Zoom meetings. Use this instead of delete-event or delete-events for booking-backed events. If Zoom needs review, first confirm the meeting was canceled or no meeting exists.",
11
+ schema: z.object({
12
+ id: z.string().min(1).describe("Booking id"),
13
+ zoomMeetingResolved: z
14
+ .boolean()
15
+ .optional()
16
+ .describe("Confirm the Zoom meeting was checked and resolved"),
17
+ }),
12
18
  // toolCallable only gates the sandboxed tools-iframe bridge; it does not
13
19
  // remove this from the agent's own tool list. The agent chat loop still
14
20
  // needs a real gate before it can send a cancellation email and delete the
15
21
  // linked Google event on its own, so require human approval too.
16
22
  toolCallable: false,
17
23
  needsApproval: true,
18
- run: async ({ id }) => {
24
+ run: async ({ id, zoomMeetingResolved }) => {
19
25
  requireActionUserEmail();
20
- return cancelBookingById(id, getAppProductionUrl());
26
+ return cancelBookingById(id, getAppProductionUrl(), {
27
+ zoomMeetingResolved,
28
+ });
21
29
  },
22
30
  });
@@ -4,6 +4,7 @@ import { inArray } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import { needsZoomCancellationReview } from "../server/lib/zoom.js";
7
8
  import type { Booking } from "../shared/api.js";
8
9
 
9
10
  function rowToBooking(
@@ -22,9 +23,13 @@ function rowToBooking(
22
23
  | "meetingLink"
23
24
  | "meetingLinkPending"
24
25
  | "googleEventId"
26
+ | "zoomNeedsReview"
27
+ | "zoomMeetingId"
28
+ | "zoomAccountId"
25
29
  | "status"
26
30
  | "createdAt"
27
31
  >,
32
+ conferencing?: string | null,
28
33
  ): Booking {
29
34
  let fieldResponses: Record<string, string | boolean> | undefined;
30
35
  if (row.fieldResponses) {
@@ -50,6 +55,11 @@ function rowToBooking(
50
55
  meetingLinkPending:
51
56
  row.meetingLinkPending && !row.meetingLink ? true : undefined,
52
57
  googleEventId: row.googleEventId ?? undefined,
58
+ zoomNeedsReview: row.zoomNeedsReview,
59
+ zoomCancellationNeedsReview: needsZoomCancellationReview({
60
+ ...row,
61
+ conferencing,
62
+ }),
53
63
  status: row.status,
54
64
  createdAt: row.createdAt,
55
65
  };
@@ -61,11 +71,17 @@ export default defineAction({
61
71
  http: { method: "GET" },
62
72
  run: async () => {
63
73
  const accessibleLinks = await getDb()
64
- .select({ slug: schema.bookingLinks.slug })
74
+ .select({
75
+ slug: schema.bookingLinks.slug,
76
+ conferencing: schema.bookingLinks.conferencing,
77
+ })
65
78
  .from(schema.bookingLinks)
66
79
  .where(accessFilter(schema.bookingLinks, schema.bookingLinkShares));
67
80
  const slugs = accessibleLinks.map((link) => link.slug);
68
81
  if (slugs.length === 0) return [];
82
+ const conferencingBySlug = new Map(
83
+ accessibleLinks.map((link) => [link.slug, link.conferencing]),
84
+ );
69
85
 
70
86
  const rows = await getDb()
71
87
  .select({
@@ -82,13 +98,18 @@ export default defineAction({
82
98
  meetingLink: schema.bookings.meetingLink,
83
99
  meetingLinkPending: schema.bookings.meetingLinkPending,
84
100
  googleEventId: schema.bookings.googleEventId,
101
+ zoomNeedsReview: schema.bookings.zoomNeedsReview,
102
+ zoomMeetingId: schema.bookings.zoomMeetingId,
103
+ zoomAccountId: schema.bookings.zoomAccountId,
85
104
  status: schema.bookings.status,
86
105
  createdAt: schema.bookings.createdAt,
87
106
  })
88
107
  .from(schema.bookings)
89
108
  .where(inArray(schema.bookings.slug, slugs))
90
109
  .orderBy(schema.bookings.start);
91
- return rows.map(rowToBooking);
110
+ return rows.map((row) =>
111
+ rowToBooking(row, conferencingBySlug.get(row.slug)),
112
+ );
92
113
  },
93
114
  });
94
115
 
@@ -16,6 +16,7 @@ import { getDb, schema } from "../server/db/index.js";
16
16
  import { getCalendarTimezone } from "../server/lib/calendar-settings.js";
17
17
  import * as googleCalendar from "../server/lib/google-calendar.js";
18
18
  import { fetchICalEvents } from "../server/lib/ical-fetcher.js";
19
+ import { needsZoomCancellationReview } from "../server/lib/zoom.js";
19
20
  import {
20
21
  getCalendarAttendeeCount,
21
22
  getCalendarAttendeeStatusCounts,
@@ -474,6 +475,7 @@ async function listLocalBookingEvents(
474
475
  slug: schema.bookingLinks.slug,
475
476
  title: schema.bookingLinks.title,
476
477
  color: schema.bookingLinks.color,
478
+ conferencing: schema.bookingLinks.conferencing,
477
479
  })
478
480
  .from(schema.bookingLinks)
479
481
  .where(accessFilter(schema.bookingLinks, schema.bookingLinkShares));
@@ -498,6 +500,9 @@ async function listLocalBookingEvents(
498
500
  meetingLink: schema.bookings.meetingLink,
499
501
  meetingLinkPending: schema.bookings.meetingLinkPending,
500
502
  googleEventId: schema.bookings.googleEventId,
503
+ zoomNeedsReview: schema.bookings.zoomNeedsReview,
504
+ zoomMeetingId: schema.bookings.zoomMeetingId,
505
+ zoomAccountId: schema.bookings.zoomAccountId,
501
506
  status: schema.bookings.status,
502
507
  createdAt: schema.bookings.createdAt,
503
508
  })
@@ -511,36 +516,44 @@ async function listLocalBookingEvents(
511
516
  ),
512
517
  );
513
518
 
514
- return rows.map((booking) => {
515
- const link = linkBySlug.get(booking.slug);
516
- const description = [
517
- booking.notes,
518
- `Booked by ${booking.name} <${booking.email}>`,
519
- ]
520
- .filter(Boolean)
521
- .join("\n\n");
519
+ return rows
520
+ .filter((booking) => {
521
+ const link = linkBySlug.get(booking.slug);
522
+ return !needsZoomCancellationReview({
523
+ ...booking,
524
+ conferencing: link?.conferencing,
525
+ });
526
+ })
527
+ .map((booking) => {
528
+ const link = linkBySlug.get(booking.slug);
529
+ const description = [
530
+ booking.notes,
531
+ `Booked by ${booking.name} <${booking.email}>`,
532
+ ]
533
+ .filter(Boolean)
534
+ .join("\n\n");
522
535
 
523
- return {
524
- id: `booking:${booking.id}`,
525
- title:
526
- booking.eventTitle || link?.title || `Booking with ${booking.name}`,
527
- description,
528
- start: booking.start,
529
- end: booking.end,
530
- location: booking.meetingLink ?? "",
531
- allDay: false,
532
- source: "local",
533
- googleEventId: booking.googleEventId ?? undefined,
534
- meetingLink: booking.meetingLink ?? undefined,
535
- meetingLinkPending:
536
- booking.meetingLinkPending && !booking.meetingLink ? true : undefined,
537
- color: link?.color ?? undefined,
538
- status: booking.status,
539
- attendees: [{ email: booking.email, displayName: booking.name }],
540
- createdAt: booking.createdAt,
541
- updatedAt: booking.createdAt,
542
- };
543
- });
536
+ return {
537
+ id: `booking:${booking.id}`,
538
+ title:
539
+ booking.eventTitle || link?.title || `Booking with ${booking.name}`,
540
+ description,
541
+ start: booking.start,
542
+ end: booking.end,
543
+ location: booking.meetingLink ?? "",
544
+ allDay: false,
545
+ source: "local",
546
+ googleEventId: booking.googleEventId ?? undefined,
547
+ meetingLink: booking.meetingLink ?? undefined,
548
+ meetingLinkPending:
549
+ booking.meetingLinkPending && !booking.meetingLink ? true : undefined,
550
+ color: link?.color ?? undefined,
551
+ status: booking.status,
552
+ attendees: [{ email: booking.email, displayName: booking.name }],
553
+ createdAt: booking.createdAt,
554
+ updatedAt: booking.createdAt,
555
+ };
556
+ });
544
557
  }
545
558
 
546
559
  /**
@@ -1,11 +1,12 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { assertAccess } from "@agent-native/core/sharing";
3
- import { eq } from "drizzle-orm";
3
+ import { and, eq, isNull, ne, or } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
7
  import { normalizeBookingDurationInput } from "../server/lib/booking-durations.js";
8
8
  import {
9
+ parseBookingConferencingConfig,
9
10
  rowToBookingLink,
10
11
  serializeBookingHosts,
11
12
  } from "../server/lib/booking-link-utils.js";
@@ -91,6 +92,7 @@ export default defineAction({
91
92
  slug: schema.bookingLinks.slug,
92
93
  ownerEmail: schema.bookingLinks.ownerEmail,
93
94
  isActive: schema.bookingLinks.isActive,
95
+ conferencing: schema.bookingLinks.conferencing,
94
96
  })
95
97
  .from(schema.bookingLinks)
96
98
  .where(eq(schema.bookingLinks.id, args.id));
@@ -98,6 +100,30 @@ export default defineAction({
98
100
  if (!current) throw new Error("Booking link not found");
99
101
  const oldSlug = current.slug;
100
102
  const slugChanged = oldSlug !== slug;
103
+ const savedConferencing = parseBookingConferencingConfig(
104
+ current.conferencing,
105
+ );
106
+ const mayHaveZoomBookings =
107
+ savedConferencing.status === "invalid" ||
108
+ (savedConferencing.status === "valid" &&
109
+ savedConferencing.config.type === "zoom");
110
+
111
+ if (mayHaveZoomBookings && args.conferencing?.type !== "zoom") {
112
+ // Preserve the review gate before the booking link loses its Zoom type.
113
+ await getDb()
114
+ .update(schema.bookings)
115
+ .set({ zoomNeedsReview: true })
116
+ .where(
117
+ and(
118
+ eq(schema.bookings.slug, oldSlug),
119
+ ne(schema.bookings.status, "cancelled"),
120
+ or(
121
+ isNull(schema.bookings.zoomMeetingId),
122
+ isNull(schema.bookings.zoomAccountId),
123
+ ),
124
+ ),
125
+ );
126
+ }
101
127
 
102
128
  await getDb()
103
129
  .update(schema.bookingLinks)
@@ -120,9 +120,16 @@ export function useCreateBooking() {
120
120
  export function useDeleteBooking() {
121
121
  const queryClient = useQueryClient();
122
122
  return useMutation({
123
- mutationFn: async (id: string) => {
124
- const res = await fetch(appApiPath(`/api/bookings/${id}`), {
123
+ mutationFn: async (input: {
124
+ id: string;
125
+ zoomMeetingResolved?: boolean;
126
+ }) => {
127
+ const res = await fetch(appApiPath(`/api/bookings/${input.id}`), {
125
128
  method: "DELETE",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({
131
+ ...(input.zoomMeetingResolved ? { zoomMeetingResolved: true } : {}),
132
+ }),
126
133
  });
127
134
  if (!res.ok) throw new Error("Failed to cancel booking");
128
135
  return res.json();
@@ -513,6 +513,14 @@ export default {
513
513
  "Your time is reserved. The host will follow up with meeting details.",
514
514
  confirmed: "Confirmed",
515
515
  confirmedCount: "Confirmed ({{count}})",
516
+ zoomNeedsReview: "Check Zoom before retrying",
517
+ zoomCancellationNeedsReview: "Check Zoom before canceling",
518
+ zoomCancellationRequiresHostReview:
519
+ "The organizer must review the Zoom meeting before this booking can be canceled.",
520
+ zoomCancelTitle: "Check Zoom before canceling",
521
+ zoomCancelDescription:
522
+ "Zoom may have created a meeting for this booking. Check your Zoom account and cancel the meeting there if it exists. Continue only after the meeting is canceled or you confirm no meeting exists.",
523
+ zoomCancelConfirm: "I've checked Zoom",
516
524
  confirming: "Confirming",
517
525
  conferencing: "Conferencing",
518
526
  connectZoom: "Connect Zoom",
@@ -0,0 +1,7 @@
1
+ export function sortBookingsNewestFirst<T extends { start: string }>(
2
+ bookings: readonly T[],
3
+ ): T[] {
4
+ return [...bookings].sort(
5
+ (a, b) => Date.parse(b.start) - Date.parse(a.start),
6
+ );
7
+ }
@@ -1,4 +1,4 @@
1
- import type { CalendarEvent } from "@shared/api";
1
+ import type { CalendarEvent, OverlayPerson } from "@shared/api";
2
2
  export {
3
3
  GOOGLE_EVENT_COLOR_OPTIONS,
4
4
  getGoogleEventColorHex,
@@ -7,6 +7,7 @@ import type {
7
7
  CalendarColorMode,
8
8
  CalendarColorSourceKey,
9
9
  } from "./calendar-view-preferences";
10
+ import { isPersonCalendarId } from "./person-calendar";
10
11
 
11
12
  // ─── Palette (dark-mode editor inspired) ─────────────────────────────────────
12
13
 
@@ -32,6 +33,32 @@ export interface CalendarColorPreferences {
32
33
  googleCalendarColors?: Record<string, string>;
33
34
  }
34
35
 
36
+ export function applyOverlayOwnerMarkers(
37
+ events: CalendarEvent[],
38
+ people: OverlayPerson[],
39
+ ): CalendarEvent[] {
40
+ const ownersByEmail = new Map(
41
+ people.map((person) => [person.email.trim().toLowerCase(), person]),
42
+ );
43
+
44
+ return events.map((event) => {
45
+ const ownerEmail =
46
+ event.overlayEmail ??
47
+ (event.source === "google" &&
48
+ event.calendarPrimary === false &&
49
+ event.calendarId &&
50
+ isPersonCalendarId(event.calendarId)
51
+ ? event.calendarId
52
+ : undefined);
53
+ const owner = ownerEmail
54
+ ? ownersByEmail.get(ownerEmail.trim().toLowerCase())
55
+ : undefined;
56
+ return owner
57
+ ? { ...event, ownerColor: owner.color, ownerName: owner.name }
58
+ : event;
59
+ });
60
+ }
61
+
35
62
  // ─── Free email providers (skip internal/external when user is on one) ───────
36
63
 
37
64
  const FREE_DOMAINS = new Set([
@@ -135,9 +162,7 @@ export function getEventDisplayColor(
135
162
  event: CalendarEvent,
136
163
  preferences?: CalendarColorPreferences,
137
164
  ): string {
138
- if (event.overlayEmail && event.ownerColor) {
139
- return event.ownerColor;
140
- }
165
+ if (event.ownerColor) return event.ownerColor;
141
166
 
142
167
  if (event.source === "google" && !event.overlayEmail && preferences) {
143
168
  const sourceColor = event.canonicalKey
@@ -5,6 +5,17 @@ import { format, parseISO } from "date-fns";
5
5
  import { useMemo, useState } from "react";
6
6
  import { toast } from "sonner";
7
7
 
8
+ import {
9
+ AlertDialog,
10
+ AlertDialogAction,
11
+ AlertDialogCancel,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogHeader,
16
+ AlertDialogTitle,
17
+ AlertDialogTrigger,
18
+ } from "@/components/ui/alert-dialog";
8
19
  import { Badge } from "@/components/ui/badge";
9
20
  import { Button } from "@/components/ui/button";
10
21
  import {
@@ -30,6 +41,7 @@ import {
30
41
  } from "@/components/ui/tooltip";
31
42
  import { useBookingLinks } from "@/hooks/use-booking-links";
32
43
  import { useBookings, useDeleteBooking } from "@/hooks/use-bookings";
44
+ import { sortBookingsNewestFirst } from "@/lib/booking-sorting";
33
45
 
34
46
  type FilterStatus = "all" | "confirmed" | "cancelled";
35
47
 
@@ -51,16 +63,27 @@ export default function BookingsList() {
51
63
  return map;
52
64
  }, [bookingLinks]);
53
65
 
54
- const filtered = bookings.filter((b) => {
55
- if (filter === "all") return true;
56
- return b.status === filter;
57
- });
66
+ const filtered = sortBookingsNewestFirst(
67
+ bookings.filter(
68
+ (b) =>
69
+ filter === "all" ||
70
+ (filter === "confirmed"
71
+ ? b.status === "confirmed" && !b.zoomNeedsReview
72
+ : b.status === filter),
73
+ ),
74
+ );
58
75
 
59
- function handleCancel(booking: Booking) {
60
- deleteBooking.mutate(booking.id, {
61
- onSuccess: () => toast.success(t("bookingLinks.bookingCancelled")),
62
- onError: () => toast.error(t("bookingLinks.failedToCancelBooking")),
63
- });
76
+ function handleCancel(booking: Booking, zoomMeetingResolved = false) {
77
+ deleteBooking.mutate(
78
+ {
79
+ id: booking.id,
80
+ ...(zoomMeetingResolved ? { zoomMeetingResolved } : {}),
81
+ },
82
+ {
83
+ onSuccess: () => toast.success(t("bookingLinks.bookingCancelled")),
84
+ onError: () => toast.error(t("bookingLinks.failedToCancelBooking")),
85
+ },
86
+ );
64
87
  }
65
88
 
66
89
  return (
@@ -72,7 +95,9 @@ export default function BookingsList() {
72
95
  </TabsTrigger>
73
96
  <TabsTrigger value="confirmed">
74
97
  {t("bookingLinks.confirmedCount", {
75
- count: bookings.filter((b) => b.status === "confirmed").length,
98
+ count: bookings.filter(
99
+ (b) => b.status === "confirmed" && !b.zoomNeedsReview,
100
+ ).length,
76
101
  })}
77
102
  </TabsTrigger>
78
103
  <TabsTrigger value="cancelled">
@@ -112,7 +137,9 @@ export default function BookingsList() {
112
137
  <TableHead>{t("bookingLinks.name")}</TableHead>
113
138
  <TableHead>{t("bookingLinks.email")}</TableHead>
114
139
  <TableHead>{t("eventForm.event")}</TableHead>
115
- <TableHead>{t("bookingLinks.dateAndTime")}</TableHead>
140
+ <TableHead className="min-w-[180px]">
141
+ {t("bookingLinks.dateAndTime")}
142
+ </TableHead>
116
143
  <TableHead>{t("bookingLinks.details")}</TableHead>
117
144
  <TableHead>{t("bookingLinks.status")}</TableHead>
118
145
  <TableHead className="w-[80px]" />
@@ -126,7 +153,7 @@ export default function BookingsList() {
126
153
  {booking.email}
127
154
  </TableCell>
128
155
  <TableCell>{booking.eventTitle}</TableCell>
129
- <TableCell className="text-muted-foreground">
156
+ <TableCell className="min-w-[180px] whitespace-nowrap text-muted-foreground">
130
157
  <div>{format(parseISO(booking.start), "MMM d, yyyy")}</div>
131
158
  <div className="text-xs">
132
159
  {format(parseISO(booking.start), "h:mm a")} -{" "}
@@ -143,14 +170,24 @@ export default function BookingsList() {
143
170
  <div className="flex items-center gap-1.5">
144
171
  <Badge
145
172
  variant={
173
+ (booking.zoomNeedsReview ||
174
+ booking.zoomCancellationNeedsReview) &&
146
175
  booking.status === "confirmed"
147
- ? "default"
148
- : "secondary"
176
+ ? "destructive"
177
+ : booking.status === "confirmed"
178
+ ? "default"
179
+ : "secondary"
149
180
  }
150
181
  >
151
- {booking.status === "confirmed"
152
- ? t("bookingLinks.confirmed")
153
- : t("bookingLinks.cancelled")}
182
+ {booking.status === "confirmed" &&
183
+ booking.zoomCancellationNeedsReview
184
+ ? t("bookingLinks.zoomCancellationNeedsReview")
185
+ : booking.status === "confirmed" &&
186
+ booking.zoomNeedsReview
187
+ ? t("bookingLinks.zoomNeedsReview")
188
+ : booking.status === "confirmed"
189
+ ? t("bookingLinks.confirmed")
190
+ : t("bookingLinks.cancelled")}
154
191
  </Badge>
155
192
  {booking.meetingLinkPending && (
156
193
  <Tooltip>
@@ -177,12 +214,55 @@ export default function BookingsList() {
177
214
  </div>
178
215
  </TableCell>
179
216
  <TableCell>
180
- {booking.status === "confirmed" && (
217
+ {booking.status === "confirmed" &&
218
+ booking.zoomCancellationNeedsReview ? (
219
+ <AlertDialog>
220
+ <Tooltip>
221
+ <TooltipTrigger asChild>
222
+ <AlertDialogTrigger asChild>
223
+ <Button
224
+ variant="ghost"
225
+ size="icon"
226
+ aria-label={t("bookingLinks.cancelBooking")}
227
+ disabled={deleteBooking.isPending}
228
+ >
229
+ <IconCircleX className="h-4 w-4 text-destructive" />
230
+ </Button>
231
+ </AlertDialogTrigger>
232
+ </TooltipTrigger>
233
+ <TooltipContent>
234
+ {t("bookingLinks.cancelBooking")}
235
+ </TooltipContent>
236
+ </Tooltip>
237
+ <AlertDialogContent>
238
+ <AlertDialogHeader>
239
+ <AlertDialogTitle>
240
+ {t("bookingLinks.zoomCancelTitle")}
241
+ </AlertDialogTitle>
242
+ <AlertDialogDescription>
243
+ {t("bookingLinks.zoomCancelDescription")}
244
+ </AlertDialogDescription>
245
+ </AlertDialogHeader>
246
+ <AlertDialogFooter>
247
+ <AlertDialogCancel>
248
+ {t("eventDialog.cancel")}
249
+ </AlertDialogCancel>
250
+ <AlertDialogAction
251
+ onClick={() => handleCancel(booking, true)}
252
+ disabled={deleteBooking.isPending}
253
+ >
254
+ {t("bookingLinks.zoomCancelConfirm")}
255
+ </AlertDialogAction>
256
+ </AlertDialogFooter>
257
+ </AlertDialogContent>
258
+ </AlertDialog>
259
+ ) : booking.status === "confirmed" ? (
181
260
  <Tooltip>
182
261
  <TooltipTrigger asChild>
183
262
  <Button
184
263
  variant="ghost"
185
264
  size="icon"
265
+ aria-label={t("bookingLinks.cancelBooking")}
186
266
  onClick={() => handleCancel(booking)}
187
267
  disabled={deleteBooking.isPending}
188
268
  >
@@ -193,7 +273,7 @@ export default function BookingsList() {
193
273
  {t("bookingLinks.cancelBooking")}
194
274
  </TooltipContent>
195
275
  </Tooltip>
196
- )}
276
+ ) : null}
197
277
  </TableCell>
198
278
  </TableRow>
199
279
  ))}
@@ -126,7 +126,10 @@ import {
126
126
  normalizeNumberOfDays,
127
127
  } from "@/lib/calendar-view-preferences";
128
128
  import { resolveEventAccountEmail } from "@/lib/event-account-selection";
129
- import { getGoogleEventColorHex } from "@/lib/event-colors";
129
+ import {
130
+ applyOverlayOwnerMarkers,
131
+ getGoogleEventColorHex,
132
+ } from "@/lib/event-colors";
130
133
  import {
131
134
  buildEventTitleUpdate,
132
135
  dateTimeInTimezoneToIso,
@@ -804,20 +807,11 @@ export default function CalendarView() {
804
807
 
805
808
  // Apply overlay ownership markers and filter hidden calendars
806
809
  const events = useMemo(() => {
807
- const ownerMap = new Map(overlayPeople.map((p) => [p.email, p]));
808
810
  const sourceEvents = draftEvent
809
811
  ? [...rawEvents.filter((e) => e.id !== draftEvent.id), draftEvent]
810
812
  : rawEvents;
811
- return sourceEvents
813
+ return applyOverlayOwnerMarkers(sourceEvents, overlayPeople)
812
814
  .map((e) => {
813
- if (e.overlayEmail && ownerMap.has(e.overlayEmail)) {
814
- const owner = ownerMap.get(e.overlayEmail);
815
- return {
816
- ...e,
817
- ownerColor: owner?.color,
818
- ownerName: owner?.name,
819
- };
820
- }
821
815
  const tempId = quickEditTempIds[e.id];
822
816
  return tempId && !e._tempId ? { ...e, _tempId: tempId } : e;
823
817
  })
@@ -34,6 +34,7 @@ interface BookingInfo {
34
34
  end: string;
35
35
  slug: string;
36
36
  meetingLink?: string;
37
+ zoomCancellationNeedsReview?: boolean;
37
38
  meetingLinkPending?: boolean;
38
39
  status: "confirmed" | "cancelled";
39
40
  }
@@ -164,6 +165,10 @@ export function ManageBookingPage() {
164
165
  <p className="text-center text-sm text-muted-foreground">
165
166
  {t("manageBooking.pastMeeting")}
166
167
  </p>
168
+ ) : booking.zoomCancellationNeedsReview ? (
169
+ <p className="text-center text-sm text-muted-foreground">
170
+ {t("bookingLinks.zoomCancellationRequiresHostReview")}
171
+ </p>
167
172
  ) : (
168
173
  <div className="flex flex-col gap-3">
169
174
  {booking.slug && (
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Bookings with an uncertain Zoom response stay reserved for review without appearing as confirmed calendar events.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ The bookings list shows the latest bookings first and keeps dates and times together.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Canceling a booking now resolves its Zoom meeting first.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Colleague events use the saved accent color shown in the calendar sidebar.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Public booking pages reject malformed meeting settings before reserving a slot.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Shared booking cancellations require editor access, and unresolved Zoom meetings stay protected when meeting settings change.
@@ -30,6 +30,11 @@ export const bookings = table("bookings", {
30
30
  calendarAccountId: text("calendar_account_id"),
31
31
  /** Token for public cancel/reschedule link */
32
32
  cancelToken: text("cancel_token"),
33
+ /** Zoom may have created the meeting before the booking request failed. */
34
+ zoomNeedsReview: boolean("zoom_needs_review").notNull().default(false),
35
+ /** Zoom provider identifiers are required to clean up a known meeting. */
36
+ zoomMeetingId: text("zoom_meeting_id"),
37
+ zoomAccountId: text("zoom_account_id"),
33
38
  status: text("status", { enum: ["confirmed", "cancelled"] })
34
39
  .notNull()
35
40
  .default("confirmed"),