@luckydye/calendar 1.3.1 → 1.3.2

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.
@@ -429,7 +429,8 @@ export class GoogleCalendarSource implements CalendarSource {
429
429
  /**
430
430
  * Update an existing event in Google Calendar.
431
431
  */
432
- async updateEvent(id: string, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
432
+ async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
433
+ const id = event.id;
433
434
  const isRsvpOnly = Object.keys(updates).length === 1 && updates.attendees !== undefined;
434
435
 
435
436
  if (isRsvpOnly) {
package/src/ICal.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { CalendarCredentials } from './CalendarIntegration.js';
2
2
  import type { CalendarEvent, Attendee, Organizer, NotificationConfig } from './CalendarInternal.js';
3
+ import { sanitizeEventDescription } from './DescriptionSanitizer.js';
3
4
 
4
5
  export interface ParseResult {
5
6
  events: CalendarEvent[];
@@ -50,7 +51,9 @@ export function parseICalEventsWithNotifications(icalText: string, color: string
50
51
  end: (currentEvent.end instanceof Date ? currentEvent.end : currentEvent.start) as Date,
51
52
  color,
52
53
  calendar,
53
- description: currentEvent.description ? String(currentEvent.description) : undefined,
54
+ description: currentEvent.description
55
+ ? sanitizeEventDescription(String(currentEvent.description))
56
+ : undefined,
54
57
  location: currentEvent.location ? String(currentEvent.location) : undefined,
55
58
  url: currentEvent.url ? String(currentEvent.url) : undefined,
56
59
  organizer: currentEvent.organizer as Organizer | undefined,
@@ -203,7 +206,9 @@ export function parseSingleICalEvent(icalText: string): Partial<CalendarEvent> {
203
206
  if (keyPart.includes("VALUE=DATE")) raw.isAllDay = true;
204
207
  break;
205
208
  case "DTEND": raw.end = parseICalDate(value, keyPart); break;
206
- case "DESCRIPTION": raw.description = value.replace(/\\n/g, "\n"); break;
209
+ case "DESCRIPTION":
210
+ raw.description = sanitizeEventDescription(value.replace(/\\n/g, "\n"));
211
+ break;
207
212
  case "LOCATION": raw.location = value; break;
208
213
  case "URL": raw.url = value; break;
209
214
  case "RRULE": raw.rrule = value.trim(); break;
@@ -44,17 +44,17 @@ export class InMemorySource implements CalendarSource {
44
44
  return newEvent;
45
45
  }
46
46
 
47
- async updateEvent(id: string, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
48
- const event = this.events.get(id);
49
- if (!event) {
50
- throw new Error(`Event ${id} not found`);
47
+ async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
48
+ const existing = this.events.get(event.id);
49
+ if (!existing) {
50
+ throw new Error(`Event ${event.id} not found`);
51
51
  }
52
52
 
53
53
  const updated = {
54
- ...event,
54
+ ...existing,
55
55
  ...updates,
56
56
  };
57
- this.events.set(id, updated);
57
+ this.events.set(event.id, updated);
58
58
  return updated;
59
59
  }
60
60
 
@@ -22,6 +22,7 @@ interface SerializedEvent {
22
22
  isAllDay?: boolean;
23
23
  reminders?: NotificationConfig[];
24
24
  visualStyle?: 'heatmap';
25
+ resourceUrl?: string;
25
26
  }
26
27
 
27
28
  interface SerializedMetadata {
@@ -328,6 +329,7 @@ export class IndexedDBStorage implements CalendarStorage {
328
329
  isAllDay: event.isAllDay,
329
330
  reminders: event.reminders,
330
331
  visualStyle: event.visualStyle,
332
+ resourceUrl: event.resourceUrl,
331
333
  };
332
334
  }
333
335
 
@@ -378,6 +380,7 @@ export class IndexedDBStorage implements CalendarStorage {
378
380
  isAllDay: serialized.isAllDay,
379
381
  reminders: serialized.reminders,
380
382
  visualStyle: serialized.visualStyle,
383
+ resourceUrl: serialized.resourceUrl,
381
384
  };
382
385
  }
383
386
 
@@ -432,11 +432,12 @@ export class InhouseBookingSource implements CalendarSource {
432
432
  /**
433
433
  * Update an existing booking in the Inhouse Booking System.
434
434
  */
435
- async updateEvent(id: string, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
435
+ async updateEvent(event: CalendarEvent, updates: Partial<CalendarEvent>): Promise<CalendarEvent> {
436
436
  if (!this.enabled) {
437
437
  throw new Error('Inhouse Booking System source is disabled');
438
438
  }
439
439
 
440
+ const id = event.id;
440
441
  const parsedId = this.parseEventId(id);
441
442
  if (!parsedId) {
442
443
  throw new Error(`Invalid event ID format: ${id}`);
package/src/app.ts CHANGED
@@ -16,6 +16,7 @@ import { fetchTimeseriesJsonEvents } from "./TimeseriesJson.js";
16
16
  import { queueStatus } from "./StatusMessage.js";
17
17
  import { activeCalendarStore } from "./ActiveCalendarStore.js";
18
18
  import type { CalendarEvent } from "./CalendarInternal.js";
19
+ import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
19
20
  import { registerKeybinds } from "./Keybinds.js";
20
21
  import "./StatusBar.js";
21
22
  import { NotificationScheduler } from "./NotificationScheduler.js";
@@ -36,6 +37,136 @@ const calendarElement = document.querySelector(
36
37
  ) as CalendarViewElement;
37
38
  const calendar = calendarElement.internal;
38
39
 
40
+ type PromptApiLanguageModel = {
41
+ availability?: () => Promise<string>;
42
+ create: (options?: unknown) => Promise<{
43
+ promptStreaming: (
44
+ prompt: string,
45
+ options?: unknown,
46
+ ) => Promise<ReadableStream<string>> | ReadableStream<string>;
47
+ destroy?: () => void;
48
+ }>;
49
+ };
50
+
51
+ function getPromptApiLanguageModel(): PromptApiLanguageModel | null {
52
+ const fromGlobal = (globalThis as { LanguageModel?: unknown }).LanguageModel;
53
+ if (fromGlobal) return fromGlobal as PromptApiLanguageModel;
54
+
55
+ const fromWindow = (
56
+ window as { ai?: { languageModel?: unknown } }
57
+ ).ai?.languageModel;
58
+ if (!fromWindow) return null;
59
+ return fromWindow as PromptApiLanguageModel;
60
+ }
61
+
62
+ let summaryAbortController: AbortController | null = null;
63
+ let summarySession: { destroy?: () => void } | null = null;
64
+ let summaryRequestToken = 0;
65
+ let activeSummaryKey: string | null = null;
66
+
67
+ function abortActiveDescriptionSummary(): void {
68
+ if (summaryAbortController) {
69
+ summaryAbortController.abort();
70
+ summaryAbortController = null;
71
+ }
72
+ if (summarySession) {
73
+ summarySession.destroy?.();
74
+ summarySession = null;
75
+ }
76
+ if (activeSummaryKey) {
77
+ calendarElement.cancelDescriptionSummary(activeSummaryKey);
78
+ activeSummaryKey = null;
79
+ }
80
+ summaryRequestToken++;
81
+ }
82
+
83
+ async function streamDescriptionSummary(key: string, description: string): Promise<void> {
84
+ const sanitizedDescription = sanitizeEventDescription(description);
85
+ abortActiveDescriptionSummary();
86
+ activeSummaryKey = key;
87
+ const token = summaryRequestToken;
88
+ calendarElement.startDescriptionSummary(key);
89
+
90
+ const languageModel = getPromptApiLanguageModel();
91
+ if (!languageModel) {
92
+ calendarElement.failDescriptionSummary(key, "Prompt API not available in this browser.");
93
+ activeSummaryKey = null;
94
+ return;
95
+ }
96
+
97
+ const controller = new AbortController();
98
+ summaryAbortController = controller;
99
+ let session: { destroy?: () => void } | null = null;
100
+
101
+ try {
102
+ const availability = await languageModel.availability?.();
103
+ if (token !== summaryRequestToken || key !== activeSummaryKey) return;
104
+ if (availability === "unavailable") {
105
+ throw new Error("Prompt API model is unavailable.");
106
+ }
107
+
108
+ session = await languageModel.create();
109
+ summarySession = session;
110
+ const prompt = [
111
+ "Summarize this calendar event description in 2-3 concise sentences.",
112
+ "Keep critical details like dates, times, links, and action items.",
113
+ "Return plain text only.",
114
+ "",
115
+ sanitizedDescription,
116
+ ].join("\n");
117
+
118
+ const stream = await Promise.resolve(
119
+ session.promptStreaming(prompt, { signal: controller.signal }),
120
+ );
121
+ const reader = stream.getReader();
122
+ try {
123
+ while (true) {
124
+ const { value, done } = await reader.read();
125
+ if (done) break;
126
+ if (
127
+ token !== summaryRequestToken ||
128
+ key !== activeSummaryKey ||
129
+ controller.signal.aborted
130
+ ) {
131
+ return;
132
+ }
133
+ if (value) {
134
+ calendarElement.appendDescriptionSummaryChunk(key, value);
135
+ }
136
+ }
137
+ } finally {
138
+ reader.releaseLock();
139
+ }
140
+
141
+ if (token !== summaryRequestToken || key !== activeSummaryKey) return;
142
+ calendarElement.finishDescriptionSummary(key);
143
+ activeSummaryKey = null;
144
+ } catch (error) {
145
+ if (
146
+ controller.signal.aborted ||
147
+ token !== summaryRequestToken ||
148
+ key !== activeSummaryKey
149
+ ) {
150
+ return;
151
+ }
152
+ calendarElement.failDescriptionSummary(
153
+ key,
154
+ error instanceof Error
155
+ ? error.message
156
+ : "Failed to generate description summary.",
157
+ );
158
+ activeSummaryKey = null;
159
+ } finally {
160
+ if (summaryAbortController === controller) {
161
+ summaryAbortController = null;
162
+ }
163
+ if (summarySession === session) {
164
+ summarySession = null;
165
+ }
166
+ session?.destroy?.();
167
+ }
168
+ }
169
+
39
170
  registerKeybinds(
40
171
  [
41
172
  { key: "r", cmdOrCtrl: true, shift: true, action: () => calendarElement.forceSync() },
@@ -109,6 +240,21 @@ calendarElement.addEventListener("event-click", () => {
109
240
  requestNotificationPermission();
110
241
  }, { once: true });
111
242
 
243
+ calendarElement.addEventListener("request-description-summary", (e: Event) => {
244
+ const { key, description } = (e as CustomEvent).detail as {
245
+ key: string;
246
+ description: string;
247
+ };
248
+ void streamDescriptionSummary(key, description);
249
+ });
250
+
251
+ calendarElement.addEventListener("cancel-description-summary", (e: Event) => {
252
+ const { key } = (e as CustomEvent).detail as { key: string };
253
+ if (key === activeSummaryKey) {
254
+ abortActiveDescriptionSummary();
255
+ }
256
+ });
257
+
112
258
  // Load iCal files
113
259
  const integration = new CalendarIntegration(calendar);
114
260
 
@@ -277,10 +423,10 @@ async function createCalDAVCalendars(
277
423
  },
278
424
 
279
425
  async updateEvent(
280
- id: string,
426
+ event: CalendarEvent,
281
427
  updates: Partial<CalendarEvent>,
282
428
  ): Promise<CalendarEvent> {
283
- return caldavSource.updateEvent(id, { ...updates, calendarId: calInfo.url });
429
+ return caldavSource.updateEvent(event, { ...updates, calendarId: calInfo.url });
284
430
  },
285
431
 
286
432
  async deleteEvent(id: string): Promise<void> {
@@ -372,10 +518,10 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
372
518
  },
373
519
 
374
520
  async updateEvent(
375
- id: string,
521
+ event: CalendarEvent,
376
522
  updates: Partial<CalendarEvent>,
377
523
  ): Promise<CalendarEvent> {
378
- return googleSource.updateEvent(id, updates);
524
+ return googleSource.updateEvent(event, updates);
379
525
  },
380
526
 
381
527
  async deleteEvent(id: string): Promise<void> {
@@ -419,10 +565,10 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
419
565
  },
420
566
 
421
567
  async updateEvent(
422
- id: string,
568
+ event: CalendarEvent,
423
569
  updates: Partial<CalendarEvent>,
424
570
  ): Promise<CalendarEvent> {
425
- return inhouseSource.updateEvent(id, updates);
571
+ return inhouseSource.updateEvent(event, updates);
426
572
  },
427
573
 
428
574
  async deleteEvent(id: string): Promise<void> {
@@ -458,12 +604,12 @@ function createInMemoryCalendar(source: InMemorySource): Calendar {
458
604
  },
459
605
 
460
606
  async updateEvent(
461
- id: string,
607
+ event: CalendarEvent,
462
608
  updates: Partial<CalendarEvent>,
463
609
  ): Promise<CalendarEvent> {
464
610
  if (!source.updateEvent)
465
611
  throw new Error("Source does not support updateEvent");
466
- return source.updateEvent(id, updates);
612
+ return source.updateEvent(event, updates);
467
613
  },
468
614
 
469
615
  async deleteEvent(id: string): Promise<void> {
@@ -806,41 +952,6 @@ calendarElement.addEventListener("create-event", async (e) => {
806
952
  }
807
953
  });
808
954
 
809
- calendarElement.addEventListener("move-event", async (e) => {
810
- const { event, start, end } = e.detail;
811
-
812
- const calendar = findCalendarForEvent(event);
813
-
814
- if (!calendar) {
815
- queueStatus("Cannot move event: calendar not found.");
816
- return;
817
- }
818
-
819
- if (!calendar.updateEvent) {
820
- queueStatus(`Calendar "${calendar.name}" does not support moving events.`);
821
- return;
822
- }
823
-
824
- console.log(
825
- "Moving event:",
826
- event.id,
827
- "from",
828
- event.start,
829
- "to",
830
- start,
831
- "on calendar:",
832
- calendar.name,
833
- );
834
- try {
835
- calendarElement.internal.applyEventOptimistically({ ...event, start, end });
836
- await withMutation(calendar, () => calendar.updateEvent(event.id, { start, end }));
837
- } catch (error) {
838
- calendarElement.internal.applyEventOptimistically(event);
839
- console.error("Failed to move event:", error);
840
- queueStatus(`Failed to move event: ${error instanceof Error ? error.message : String(error)}`);
841
- }
842
- });
843
-
844
955
  calendarElement.addEventListener("update-event", async (e) => {
845
956
  const { event, updates } = e.detail;
846
957
 
@@ -864,7 +975,7 @@ calendarElement.addEventListener("update-event", async (e) => {
864
975
  );
865
976
  try {
866
977
  calendarElement.internal.applyEventOptimistically({ ...event, ...updates });
867
- await withMutation(calendar, () => calendar.updateEvent(event.id, updates));
978
+ await withMutation(calendar, () => calendar.updateEvent(event, updates));
868
979
 
869
980
  if ("reminders" in updates) {
870
981
  const eventWithReminders = { ...event, ...updates };