@agent-native/core 0.172.2 → 0.172.4

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 (51) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/get-settings.ts +2 -5
  3. package/corpus/templates/calendar/actions/list-events.ts +21 -80
  4. package/corpus/templates/calendar/actions/update-settings.ts +11 -16
  5. package/corpus/templates/calendar/actions/view-screen.ts +25 -10
  6. package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +7 -1
  7. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +5 -2
  8. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +3 -18
  9. package/corpus/templates/calendar/app/lib/event-form-utils.ts +3 -62
  10. package/corpus/templates/calendar/changelog/2026-07-22-calendar-grid-local-time.md +6 -0
  11. package/corpus/templates/calendar/changelog/2026-08-19-the-calendar-loads-again-for-accounts-whose-saved-timezone-w.md +6 -0
  12. package/corpus/templates/calendar/server/handlers/settings.ts +8 -24
  13. package/corpus/templates/calendar/server/lib/calendar-settings.ts +58 -0
  14. package/corpus/templates/calendar/server/lib/find-time.ts +15 -93
  15. package/corpus/templates/calendar/shared/settings.ts +15 -9
  16. package/corpus/templates/calendar/shared/timezone.ts +127 -0
  17. package/corpus/templates/clips/actions/lib/finalize-ended-meetings.ts +37 -0
  18. package/corpus/templates/clips/actions/request-transcript.ts +4 -0
  19. package/corpus/templates/clips/actions/save-browser-transcript.ts +4 -0
  20. package/corpus/templates/clips/desktop/src/app.tsx +436 -365
  21. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +17 -2
  22. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +21 -2
  23. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +1 -0
  24. package/corpus/templates/clips/desktop/src/styles.css +17 -3
  25. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +114 -20
  26. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +130 -15
  27. package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -4
  28. package/corpus/templates/content/actions/_database-row-mutation.ts +22 -2
  29. package/corpus/templates/slides/actions/list-decks.ts +79 -5
  30. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +21 -1
  31. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +49 -21
  32. package/corpus/templates/slides/app/context/DeckContext.tsx +49 -21
  33. package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
  34. package/corpus/templates/slides/app/pages/Index.tsx +11 -13
  35. package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +5 -0
  36. package/dist/agent/production-agent.js +5 -1
  37. package/dist/agent/types.d.ts +5 -0
  38. package/dist/collab/routes.d.ts +1 -1
  39. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  40. package/dist/observability/posthog-ai.d.ts +36 -11
  41. package/dist/observability/posthog-ai.js +99 -30
  42. package/dist/observability/routes.js +9 -2
  43. package/dist/observability/traces.js +330 -142
  44. package/dist/progress/routes.d.ts +1 -1
  45. package/dist/resources/handlers.d.ts +3 -3
  46. package/dist/secrets/routes.d.ts +6 -6
  47. package/dist/server/realtime-token.d.ts +1 -1
  48. package/dist/tracking/providers.js +9 -3
  49. package/dist/vite/client.js +1 -0
  50. package/docs/content/observability.mdx +27 -10
  51. package/package.json +3 -3
@@ -3,6 +3,12 @@ import type {
3
3
  FindTimeParticipant,
4
4
  FindTimeSlot,
5
5
  } from "../../shared/api.js";
6
+ import {
7
+ addDaysToDateKey,
8
+ dateKeyInTimezone,
9
+ dateTimeInTimezoneToIso,
10
+ isCalendarTimezone,
11
+ } from "../../shared/timezone.js";
6
12
 
7
13
  export interface AvailabilitySchedule {
8
14
  timezone: string;
@@ -37,108 +43,24 @@ const DEFAULT_SCHEDULE: AvailabilitySchedule["schedule"] = {
37
43
  sunday: [],
38
44
  };
39
45
 
46
+ /**
47
+ * These live in shared/timezone.ts so the grid, the actions, and this module
48
+ * cannot drift on DST edges. Kept under their original names because callers
49
+ * across the template import them from here.
50
+ */
40
51
  export function normalizeTimezone(timezone?: string): string {
41
- if (!timezone) return "UTC";
42
- try {
43
- new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
44
- return timezone;
45
- } catch {
46
- return "UTC";
47
- }
48
- }
49
-
50
- function datePartsInTimezone(date: Date, timezone: string) {
51
- const parts = new Intl.DateTimeFormat("en-US", {
52
- timeZone: timezone,
53
- hourCycle: "h23",
54
- year: "numeric",
55
- month: "2-digit",
56
- day: "2-digit",
57
- hour: "2-digit",
58
- minute: "2-digit",
59
- second: "2-digit",
60
- }).formatToParts(date);
61
- const get = (type: string) =>
62
- Number(parts.find((part) => part.type === type)?.value ?? "0");
63
- return {
64
- year: get("year"),
65
- month: get("month"),
66
- day: get("day"),
67
- hour: get("hour"),
68
- minute: get("minute"),
69
- second: get("second"),
70
- };
52
+ return isCalendarTimezone(timezone) ? timezone : "UTC";
71
53
  }
72
54
 
73
- export function dateOnlyInTimezone(date: Date, timezone: string): string {
74
- const parts = datePartsInTimezone(date, timezone);
75
- return [
76
- String(parts.year).padStart(4, "0"),
77
- String(parts.month).padStart(2, "0"),
78
- String(parts.day).padStart(2, "0"),
79
- ].join("-");
80
- }
81
-
82
- export function addDaysToDateOnly(dateOnly: string, days: number): string {
83
- const [year, month, day] = dateOnly.split("-").map(Number);
84
- const date = new Date(Date.UTC(year, month - 1, day + days));
85
- return [
86
- String(date.getUTCFullYear()).padStart(4, "0"),
87
- String(date.getUTCMonth() + 1).padStart(2, "0"),
88
- String(date.getUTCDate()).padStart(2, "0"),
89
- ].join("-");
90
- }
91
-
92
- function offsetMsForTimezone(date: Date, timezone: string): number {
93
- const parts = datePartsInTimezone(date, timezone);
94
- const asUtc = Date.UTC(
95
- parts.year,
96
- parts.month - 1,
97
- parts.day,
98
- parts.hour,
99
- parts.minute,
100
- parts.second,
101
- );
102
- return asUtc - date.getTime();
103
- }
55
+ export const dateOnlyInTimezone = dateKeyInTimezone;
56
+ export const addDaysToDateOnly = addDaysToDateKey;
104
57
 
105
58
  export function zonedDateTimeToUtcIso(
106
59
  dateOnly: string,
107
60
  time: string,
108
61
  timezone: string,
109
62
  ): string {
110
- const [year, month, day] = dateOnly.split("-").map(Number);
111
- const [hour, minute] = time.split(":").map(Number);
112
- const wallClockUtc = Date.UTC(year, month - 1, day, hour, minute, 0);
113
- const offsets = new Set<number>();
114
- for (let hours = -36; hours <= 36; hours += 6) {
115
- offsets.add(
116
- offsetMsForTimezone(
117
- new Date(wallClockUtc + hours * 60 * 60 * 1000),
118
- timezone,
119
- ),
120
- );
121
- }
122
-
123
- const candidates = [...offsets]
124
- .map((offset) => new Date(wallClockUtc - offset))
125
- .map((candidate) => ({
126
- candidate,
127
- localWallClock:
128
- candidate.getTime() + offsetMsForTimezone(candidate, timezone),
129
- }))
130
- .sort((a, b) => {
131
- const aDelta = a.localWallClock - wallClockUtc;
132
- const bDelta = b.localWallClock - wallClockUtc;
133
- if (aDelta === 0 && bDelta === 0) {
134
- return a.candidate.getTime() - b.candidate.getTime();
135
- }
136
- if (aDelta >= 0 && bDelta < 0) return -1;
137
- if (aDelta < 0 && bDelta >= 0) return 1;
138
- return Math.abs(aDelta) - Math.abs(bDelta);
139
- });
140
-
141
- return candidates[0].candidate.toISOString();
63
+ return dateTimeInTimezoneToIso(dateOnly, time, timezone);
142
64
  }
143
65
 
144
66
  function normalizeDateBound(value: string, timezone: string): string {
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_CALENDAR_WEEK_START,
4
4
  isCalendarWeekStart,
5
5
  } from "./calendar-week.js";
6
+ import { isCalendarTimezone } from "./timezone.js";
6
7
 
7
8
  export const DEFAULT_SETTINGS: Settings = {
8
9
  timezone: "America/New_York",
@@ -12,33 +13,38 @@ export const DEFAULT_SETTINGS: Settings = {
12
13
  weekStart: DEFAULT_CALENDAR_WEEK_START,
13
14
  };
14
15
 
15
- export function normalizeCalendarSettings(input: unknown): Settings {
16
+ export function normalizeCalendarSettings(
17
+ input: unknown,
18
+ fallbacks?: Partial<Settings>,
19
+ ): Settings {
20
+ const defaults = fallbacks
21
+ ? normalizeCalendarSettings(fallbacks)
22
+ : DEFAULT_SETTINGS;
16
23
  const raw =
17
24
  input && typeof input === "object"
18
25
  ? (input as Partial<Settings>)
19
26
  : ({} as Partial<Settings>);
20
27
 
21
28
  return {
22
- timezone:
23
- typeof raw.timezone === "string"
24
- ? raw.timezone
25
- : DEFAULT_SETTINGS.timezone,
29
+ timezone: isCalendarTimezone(raw.timezone)
30
+ ? raw.timezone
31
+ : defaults.timezone,
26
32
  bookingPageTitle:
27
33
  typeof raw.bookingPageTitle === "string"
28
34
  ? raw.bookingPageTitle
29
- : DEFAULT_SETTINGS.bookingPageTitle,
35
+ : defaults.bookingPageTitle,
30
36
  bookingPageDescription:
31
37
  typeof raw.bookingPageDescription === "string"
32
38
  ? raw.bookingPageDescription
33
- : DEFAULT_SETTINGS.bookingPageDescription,
39
+ : defaults.bookingPageDescription,
34
40
  defaultEventDuration:
35
41
  typeof raw.defaultEventDuration === "number" &&
36
42
  Number.isFinite(raw.defaultEventDuration) &&
37
43
  raw.defaultEventDuration > 0
38
44
  ? raw.defaultEventDuration
39
- : DEFAULT_SETTINGS.defaultEventDuration,
45
+ : defaults.defaultEventDuration,
40
46
  weekStart: isCalendarWeekStart(raw.weekStart)
41
47
  ? raw.weekStart
42
- : DEFAULT_SETTINGS.weekStart,
48
+ : defaults.weekStart,
43
49
  };
44
50
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Constructing an `Intl.DateTimeFormat` costs ~45µs, and resolving one wall
3
+ * clock probes the zone a dozen times — so formatters are cached per zone and
4
+ * option set. Only the date varies per call, and that is an argument to
5
+ * `format`/`formatToParts`, never part of the formatter.
6
+ */
7
+ const formatterCache = new Map<string, Intl.DateTimeFormat>();
8
+
9
+ export function timezoneFormatter(
10
+ timezone: string | undefined,
11
+ options: Intl.DateTimeFormatOptions,
12
+ locale?: string,
13
+ ): Intl.DateTimeFormat {
14
+ const key = `${locale ?? ""}\u0000${timezone ?? ""}\u0000${JSON.stringify(options)}`;
15
+ let formatter = formatterCache.get(key);
16
+ if (!formatter) {
17
+ formatter = new Intl.DateTimeFormat(locale, {
18
+ ...options,
19
+ timeZone: timezone,
20
+ });
21
+ formatterCache.set(key, formatter);
22
+ }
23
+ return formatter;
24
+ }
25
+
26
+ /**
27
+ * Whether a value names a zone this calendar can use. Only a `RangeError` means
28
+ * Intl rejected the zone; any other failure is a real fault and must surface
29
+ * instead of being reported as "invalid".
30
+ *
31
+ * Several older helpers around the template still run their own version of this
32
+ * check with a bare `catch`; prefer this one and delete those as you touch them.
33
+ */
34
+ export function isCalendarTimezone(value: unknown): value is string {
35
+ if (typeof value !== "string" || !value.trim()) return false;
36
+ try {
37
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format();
38
+ return true;
39
+ } catch (error) {
40
+ if (error instanceof RangeError) return false;
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ const OFFSET_PROBE_OPTIONS: Intl.DateTimeFormatOptions = {
46
+ hourCycle: "h23",
47
+ year: "numeric",
48
+ month: "2-digit",
49
+ day: "2-digit",
50
+ hour: "2-digit",
51
+ minute: "2-digit",
52
+ second: "2-digit",
53
+ };
54
+
55
+ function offsetMsInTimezone(date: Date, timezone: string): number {
56
+ const parts = timezoneFormatter(
57
+ timezone,
58
+ OFFSET_PROBE_OPTIONS,
59
+ "en-US",
60
+ ).formatToParts(date);
61
+ const values = new Map(parts.map((part) => [part.type, part.value]));
62
+ const asUtc = Date.UTC(
63
+ Number(values.get("year")),
64
+ Number(values.get("month")) - 1,
65
+ Number(values.get("day")),
66
+ Number(values.get("hour")),
67
+ Number(values.get("minute")),
68
+ Number(values.get("second")),
69
+ );
70
+ return asUtc - date.getTime();
71
+ }
72
+
73
+ export function dateTimeInTimezoneToIso(
74
+ date: string,
75
+ time: string,
76
+ timezone: string,
77
+ ): string {
78
+ const [year, month, day] = date.split("-").map(Number);
79
+ const [hour, minute] = time.split(":").map(Number);
80
+ const wallClockUtc = Date.UTC(year, month - 1, day, hour, minute, 0);
81
+ const offsets = new Set<number>();
82
+ for (let hours = -36; hours <= 36; hours += 6) {
83
+ offsets.add(
84
+ offsetMsInTimezone(
85
+ new Date(wallClockUtc + hours * 60 * 60 * 1000),
86
+ timezone,
87
+ ),
88
+ );
89
+ }
90
+
91
+ const candidates = [...offsets]
92
+ .map((offset) => new Date(wallClockUtc - offset))
93
+ .map((candidate) => ({
94
+ candidate,
95
+ localWallClock:
96
+ candidate.getTime() + offsetMsInTimezone(candidate, timezone),
97
+ }))
98
+ .sort((a, b) => {
99
+ const aDelta = a.localWallClock - wallClockUtc;
100
+ const bDelta = b.localWallClock - wallClockUtc;
101
+ if (aDelta === 0 && bDelta === 0) {
102
+ return a.candidate.getTime() - b.candidate.getTime();
103
+ }
104
+ if (aDelta >= 0 && bDelta < 0) return -1;
105
+ if (aDelta < 0 && bDelta >= 0) return 1;
106
+ return Math.abs(aDelta) - Math.abs(bDelta);
107
+ });
108
+
109
+ return candidates[0].candidate.toISOString();
110
+ }
111
+
112
+ export function dateKeyInTimezone(date: Date, timezone: string): string {
113
+ const parts = timezoneFormatter(
114
+ timezone,
115
+ { year: "numeric", month: "2-digit", day: "2-digit" },
116
+ "en-CA",
117
+ ).formatToParts(date);
118
+ const value = (type: string) =>
119
+ parts.find((part) => part.type === type)!.value;
120
+ return `${value("year")}-${value("month")}-${value("day")}`;
121
+ }
122
+
123
+ export function addDaysToDateKey(date: string, amount: number): string {
124
+ const [year, month, day] = date.split("-").map(Number);
125
+ const shifted = new Date(Date.UTC(year, month - 1, day + amount));
126
+ return shifted.toISOString().slice(0, 10);
127
+ }
@@ -0,0 +1,37 @@
1
+ import { and, eq, inArray, isNotNull } from "drizzle-orm";
2
+
3
+ import { getDb, schema } from "../../server/db/index.js";
4
+ import finalizeMeeting from "../finalize-meeting.js";
5
+
6
+ /** Finalize meeting notes once a linked recording's transcript is ready. */
7
+ export async function finalizeEndedMeetingsForRecording(
8
+ db: ReturnType<typeof getDb>,
9
+ recordingId: string,
10
+ ): Promise<void> {
11
+ const meetings = await db
12
+ .select({ id: schema.meetings.id })
13
+ .from(schema.meetings)
14
+ .where(
15
+ and(
16
+ eq(schema.meetings.recordingId, recordingId),
17
+ isNotNull(schema.meetings.actualEnd),
18
+ inArray(schema.meetings.transcriptStatus, ["ready", "failed"]),
19
+ eq(schema.meetings.summaryMd, ""),
20
+ ),
21
+ );
22
+
23
+ await Promise.all(
24
+ meetings.map(async (meeting) => {
25
+ try {
26
+ await finalizeMeeting.run({ meetingId: meeting.id });
27
+ } catch (error) {
28
+ // Transcript delivery must remain successful even when the separate
29
+ // notes model is unavailable; a later transcript retry can try again.
30
+ console.warn(
31
+ `[clips] meeting notes finalization failed for ${meeting.id}:`,
32
+ (error as Error)?.message ?? String(error),
33
+ );
34
+ }
35
+ }),
36
+ );
37
+ }
@@ -80,6 +80,7 @@ import {
80
80
  clearBuilderCreditsExhausted,
81
81
  noteBuilderCreditsExhausted,
82
82
  } from "./lib/builder-credits-state.js";
83
+ import { finalizeEndedMeetingsForRecording } from "./lib/finalize-ended-meetings.js";
83
84
  import {
84
85
  fetchLoomTranscript,
85
86
  loomTranscriptUnavailableMessage,
@@ -739,6 +740,7 @@ export async function importLoomTranscriptForRecording({
739
740
  now,
740
741
  });
741
742
  await writeAppState("refresh-signal", { ts: Date.now() });
743
+ await finalizeEndedMeetingsForRecording(db, recordingId);
742
744
  await queueBrainExport(recordingId);
743
745
  return {
744
746
  recordingId,
@@ -1177,6 +1179,7 @@ async function completeReadyTranscript({
1177
1179
  // (`transcript-cleanup-${recordingId}`) before its next 2s tick lands —
1178
1180
  // otherwise the "Cleaning up…" badge can lag for one full poll interval.
1179
1181
  await writeAppState("refresh-signal", { ts: Date.now() });
1182
+ await finalizeEndedMeetingsForRecording(db, recordingId);
1180
1183
  await queueBrainExport(recordingId);
1181
1184
 
1182
1185
  return {
@@ -1580,6 +1583,7 @@ const requestTranscriptAction = defineAction({
1580
1583
  now,
1581
1584
  });
1582
1585
  await writeAppState("refresh-signal", { ts: Date.now() });
1586
+ await finalizeEndedMeetingsForRecording(db, args.recordingId);
1583
1587
  await queueBrainExport(args.recordingId);
1584
1588
  await clearBuilderCreditsExhausted();
1585
1589
 
@@ -24,6 +24,7 @@ import { dispatchPostFinalizeJob } from "../server/lib/post-finalize-dispatch.js
24
24
  import { getCurrentOwnerEmail } from "../server/lib/recordings.js";
25
25
  import { buildCaptionSegmentsFromText } from "../shared/transcript-segments.js";
26
26
  import { booleanParam } from "./lib/cli-params.js";
27
+ import { finalizeEndedMeetingsForRecording } from "./lib/finalize-ended-meetings.js";
27
28
  import { isAutoTitleReplaceable } from "./lib/title-source.js";
28
29
 
29
30
  // web-speech and macos-native are both mic-only engines — see
@@ -226,6 +227,9 @@ export default defineAction({
226
227
  );
227
228
 
228
229
  await writeAppState("refresh-signal", { ts: Date.now() });
230
+ if (savedStatus === "ready") {
231
+ await finalizeEndedMeetingsForRecording(db, args.recordingId);
232
+ }
229
233
 
230
234
  const [rec] = await db
231
235
  .select({