@agent-native/core 0.154.5 → 0.155.0

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 (185) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
  3. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
  4. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
  5. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
  6. package/corpus/templates/analytics/AGENTS.md +3 -0
  7. package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
  8. package/corpus/templates/analytics/actions/bigquery.ts +1 -1
  9. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
  10. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
  12. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
  13. package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
  14. package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
  16. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
  17. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  18. package/corpus/templates/calendar/AGENTS.md +2 -0
  19. package/corpus/templates/calendar/DEVELOPING.md +4 -4
  20. package/corpus/templates/calendar/actions/get-settings.ts +4 -11
  21. package/corpus/templates/calendar/actions/update-availability.ts +15 -4
  22. package/corpus/templates/calendar/actions/update-settings.ts +15 -4
  23. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
  24. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
  26. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
  27. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
  28. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
  29. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
  30. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
  31. package/corpus/templates/calendar/app/i18n-data.ts +30 -0
  32. package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
  33. package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
  34. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
  35. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
  36. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
  37. package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
  39. package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
  40. package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
  41. package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
  42. package/corpus/templates/calendar/shared/api.ts +1 -0
  43. package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
  44. package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
  45. package/corpus/templates/calendar/shared/settings.ts +44 -0
  46. package/corpus/templates/clips/AGENTS.md +2 -0
  47. package/corpus/templates/clips/actions/add-comment.ts +5 -2
  48. package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
  49. package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
  50. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
  51. package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
  52. package/corpus/templates/clips/actions/update-comment.ts +9 -2
  53. package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
  54. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
  55. package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
  56. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
  57. package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
  58. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
  59. package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
  60. package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
  61. package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
  62. package/corpus/templates/clips/app/root.tsx +2 -1
  63. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
  64. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
  65. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
  66. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -7
  67. package/corpus/templates/clips/app/routes/share.$shareId.tsx +8 -4
  68. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
  69. package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
  71. package/corpus/templates/clips/desktop/src/app.tsx +116 -19
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
  73. package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
  74. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
  75. package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
  76. package/corpus/templates/clips/desktop/src/styles.css +117 -0
  77. package/corpus/templates/clips/server/db/index.ts +10 -0
  78. package/corpus/templates/clips/server/db/schema.ts +1 -0
  79. package/corpus/templates/clips/server/lib/recordings.ts +3 -1
  80. package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
  81. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  82. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
  83. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
  84. package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
  85. package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
  86. package/corpus/templates/content/actions/add-comment.ts +2 -1
  87. package/corpus/templates/content/actions/update-comment.ts +1 -1
  88. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
  89. package/corpus/templates/design/actions/generate-design.ts +10 -7
  90. package/corpus/templates/design/actions/present-design-variants.ts +35 -6
  91. package/corpus/templates/design/actions/run-design-audit.ts +33 -0
  92. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
  93. package/corpus/templates/design/shared/design-review.ts +1 -0
  94. package/corpus/templates/design/shared/html-integrity.ts +66 -0
  95. package/corpus/templates/dispatch/.env.example +5 -0
  96. package/corpus/templates/dispatch/AGENTS.md +24 -19
  97. package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
  98. package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
  99. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
  100. package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
  101. package/corpus/templates/factory/AGENTS.md +5 -1
  102. package/corpus/templates/factory/README.md +23 -0
  103. package/corpus/templates/factory/actions/view-screen.ts +17 -0
  104. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
  105. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
  106. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
  107. package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
  108. package/corpus/templates/factory/app/routes/factory.tsx +41 -0
  109. package/corpus/templates/factory/package.json +1 -0
  110. package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
  111. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
  112. package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
  113. package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
  114. package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
  115. package/corpus/templates/mail/vite.config.ts +7 -0
  116. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
  117. package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
  118. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  119. package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
  120. package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
  121. package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
  122. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
  123. package/dist/agent/production-agent.js +5 -1
  124. package/dist/agent/thread-data-builder.js +17 -1
  125. package/dist/cli/skills-content/help.js +6 -4
  126. package/dist/cli/skills.js +17 -2
  127. package/dist/cli/workspacify.d.ts +5 -0
  128. package/dist/cli/workspacify.js +65 -8
  129. package/dist/client/AgentPanel.d.ts +5 -0
  130. package/dist/client/AgentPanel.js +20 -4
  131. package/dist/client/AssistantChat.d.ts +21 -0
  132. package/dist/client/AssistantChat.js +66 -68
  133. package/dist/client/active-run-state.d.ts +14 -1
  134. package/dist/client/active-run-state.js +0 -0
  135. package/dist/client/agent-chat-adapter.js +107 -5
  136. package/dist/client/analytics.js +9 -2
  137. package/dist/client/chat/tool-call-display.js +4 -4
  138. package/dist/client/chat-first/apps-rail.js +9 -4
  139. package/dist/client/chat-first/primary-nav.d.ts +2 -1
  140. package/dist/client/chat-first/primary-nav.js +3 -3
  141. package/dist/client/chat-first/types.d.ts +5 -1
  142. package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
  143. package/dist/client/markdown/InlineMarkdown.js +65 -0
  144. package/dist/client/markdown/index.d.ts +1 -0
  145. package/dist/client/markdown/index.js +1 -0
  146. package/dist/client/review/ReviewThreadPanel.js +2 -1
  147. package/dist/client/run-stream-ownership.d.ts +39 -0
  148. package/dist/client/run-stream-ownership.js +65 -0
  149. package/dist/collab/awareness.d.ts +2 -2
  150. package/dist/collab/struct-routes.d.ts +1 -1
  151. package/dist/feature-flags/index.d.ts +1 -1
  152. package/dist/feature-flags/index.js +1 -1
  153. package/dist/feature-flags/store.d.ts +10 -0
  154. package/dist/feature-flags/store.js +112 -1
  155. package/dist/guards/no-env-credentials.js +4 -0
  156. package/dist/index.d.ts +1 -1
  157. package/dist/index.js +1 -1
  158. package/dist/integrations/adapters/slack.js +5 -1
  159. package/dist/localization/default-messages.d.ts +1 -0
  160. package/dist/localization/default-messages.js +1 -0
  161. package/dist/observability/routes.d.ts +6 -6
  162. package/dist/progress/routes.d.ts +1 -1
  163. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  164. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  165. package/dist/resources/agents.js +40 -5
  166. package/dist/resources/handlers.d.ts +1 -1
  167. package/dist/resources/metadata.d.ts +10 -0
  168. package/dist/resources/metadata.js +7 -3
  169. package/dist/review/actions/create-review-comment.js +1 -1
  170. package/dist/review/actions/reply-review-comment.js +1 -1
  171. package/dist/review/actions/resolve-review-thread.js +1 -1
  172. package/dist/secrets/routes.d.ts +3 -3
  173. package/dist/server/action-routes.js +32 -6
  174. package/dist/server/agent-discovery.js +14 -3
  175. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  176. package/dist/server/core-routes-plugin.js +1 -1
  177. package/dist/server/identity-sso-migrations.d.ts +7 -0
  178. package/dist/server/identity-sso-migrations.js +36 -0
  179. package/dist/server/identity-sso-store.d.ts +3 -2
  180. package/dist/server/identity-sso-store.js +8 -3
  181. package/dist/server/realtime-token.d.ts +1 -1
  182. package/dist/server/release-migrations.js +4 -0
  183. package/dist/tracking/providers.js +13 -3
  184. package/dist/workspace-files/tool.js +1 -1
  185. package/package.json +3 -2
@@ -14,6 +14,8 @@ import {
14
14
  AppearancePicker,
15
15
  type AppearancePresetId,
16
16
  } from "@agent-native/core/client/ui";
17
+ import type { CalendarWeekStart } from "@shared/calendar-week";
18
+ import { isCalendarWeekStart } from "@shared/calendar-week";
17
19
  import {
18
20
  IconBrandZoom,
19
21
  IconExternalLink,
@@ -38,6 +40,13 @@ import {
38
40
  } from "@/components/ui/card";
39
41
  import { Input } from "@/components/ui/input";
40
42
  import { Label } from "@/components/ui/label";
43
+ import {
44
+ Select,
45
+ SelectContent,
46
+ SelectItem,
47
+ SelectTrigger,
48
+ SelectValue,
49
+ } from "@/components/ui/select";
41
50
  import { Separator } from "@/components/ui/separator";
42
51
  import { Textarea } from "@/components/ui/textarea";
43
52
  import {
@@ -56,6 +65,8 @@ import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
56
65
 
57
66
  import changelog from "../../CHANGELOG.md?raw";
58
67
 
68
+ const AVAILABILITY_SETTINGS_PATH = "/booking-links?tab=availability";
69
+
59
70
  export default function Settings() {
60
71
  const t = useT();
61
72
  const agentSettingsTabs = useAgentSettingsTabs();
@@ -83,6 +94,7 @@ export default function Settings() {
83
94
  const [bookingTitle, setBookingTitle] = useState("");
84
95
  const [bookingDescription, setBookingDescription] = useState("");
85
96
  const [defaultDuration, setDefaultDuration] = useState(30);
97
+ const [weekStart, setWeekStart] = useState<CalendarWeekStart>("sunday");
86
98
 
87
99
  useEffect(() => {
88
100
  if (settings) {
@@ -90,6 +102,9 @@ export default function Settings() {
90
102
  setBookingTitle(settings.bookingPageTitle);
91
103
  setBookingDescription(settings.bookingPageDescription);
92
104
  setDefaultDuration(settings.defaultEventDuration);
105
+ setWeekStart(
106
+ isCalendarWeekStart(settings.weekStart) ? settings.weekStart : "sunday",
107
+ );
93
108
  }
94
109
  }, [settings]);
95
110
 
@@ -100,6 +115,7 @@ export default function Settings() {
100
115
  bookingPageTitle: bookingTitle,
101
116
  bookingPageDescription: bookingDescription,
102
117
  defaultEventDuration: defaultDuration,
118
+ weekStart,
103
119
  },
104
120
  {
105
121
  onSuccess: () => toast.success(t("settings.saved")),
@@ -185,9 +201,16 @@ export default function Settings() {
185
201
  {
186
202
  id: "calendar-general",
187
203
  label: t("settings.general"),
188
- keywords: "timezone booking duration defaults general",
204
+ keywords:
205
+ "timezone week start sunday monday booking duration defaults general",
189
206
  hash: "general-settings",
190
207
  },
208
+ {
209
+ id: "calendar-availability",
210
+ label: t("bookingLinks.availability"),
211
+ keywords: "availability available hours booking schedule working hours",
212
+ hash: "availability",
213
+ },
191
214
  {
192
215
  id: "calendar-appearance",
193
216
  label: t("settings.appearance"),
@@ -240,6 +263,18 @@ export default function Settings() {
240
263
  }}
241
264
  />
242
265
  </SettingsRow>
266
+ <SettingsRow
267
+ id="availability"
268
+ label={t("bookingLinks.availability")}
269
+ description={t("bookingLinks.availabilityDescription")}
270
+ control={
271
+ <Button variant="outline" size="sm" asChild>
272
+ <Link to={AVAILABILITY_SETTINGS_PATH}>
273
+ {t("bookingLinks.availability")}
274
+ </Link>
275
+ </Button>
276
+ }
277
+ />
243
278
  </SettingsGroup>
244
279
 
245
280
  {/* Google Calendar Connection */}
@@ -412,6 +447,30 @@ export default function Settings() {
412
447
  <TimezoneCombobox value={timezone} onChange={setTimezone} />
413
448
  </div>
414
449
 
450
+ <div className="space-y-2">
451
+ <Label htmlFor="week-start">
452
+ {t("settings.weekStartLabel")}
453
+ </Label>
454
+ <Select
455
+ value={weekStart}
456
+ onValueChange={(value) => {
457
+ if (isCalendarWeekStart(value)) setWeekStart(value);
458
+ }}
459
+ >
460
+ <SelectTrigger id="week-start" className="w-full sm:w-56">
461
+ <SelectValue />
462
+ </SelectTrigger>
463
+ <SelectContent>
464
+ <SelectItem value="sunday">
465
+ {t("settings.weekStartSunday")}
466
+ </SelectItem>
467
+ <SelectItem value="monday">
468
+ {t("settings.weekStartMonday")}
469
+ </SelectItem>
470
+ </SelectContent>
471
+ </Select>
472
+ </div>
473
+
415
474
  <div className="space-y-2">
416
475
  <Label htmlFor="booking-title">
417
476
  {t("settings.bookingTitleLabel")}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-13
4
+ ---
5
+
6
+ The Find a time window now closes from a clear header action.
@@ -28,6 +28,7 @@ import type {
28
28
  CustomField,
29
29
  TimeSlot,
30
30
  } from "../../shared/api.js";
31
+ import { normalizeAvailabilitySlots } from "../../shared/availability-schedule.js";
31
32
  import { getDb, schema } from "../db/index.js";
32
33
  import {
33
34
  parseBookingLinkDurations,
@@ -626,7 +627,7 @@ export function generateAvailableSlotsForDate({
626
627
  timezone,
627
628
  );
628
629
 
629
- for (const scheduleSlot of daySchedule.slots) {
630
+ for (const scheduleSlot of normalizeAvailabilitySlots(daySchedule.slots)) {
630
631
  const [startHour, startMin] = scheduleSlot.start.split(":").map(Number);
631
632
  const [endHour, endMin] = scheduleSlot.end.split(":").map(Number);
632
633
  if (
@@ -7,14 +7,7 @@ import {
7
7
  } from "@agent-native/core/settings";
8
8
  import { defineEventHandler, setResponseStatus, type H3Event } from "h3";
9
9
 
10
- import type { Settings } from "../../shared/api.js";
11
-
12
- const DEFAULT_SETTINGS: Settings = {
13
- timezone: "America/New_York",
14
- bookingPageTitle: "Book a Meeting",
15
- bookingPageDescription: "Select a time that works for you.",
16
- defaultEventDuration: 30,
17
- };
10
+ import { normalizeCalendarSettings } from "../../shared/settings.js";
18
11
 
19
12
  async function uEmail(event: H3Event): Promise<string> {
20
13
  const session = await getSession(event);
@@ -28,9 +21,9 @@ async function uEmail(event: H3Event): Promise<string> {
28
21
  export const getSettings = defineEventHandler(async (event: H3Event) => {
29
22
  try {
30
23
  const email = await uEmail(event);
31
- const settings =
32
- (await getUserSetting(email, "calendar-settings")) || DEFAULT_SETTINGS;
33
- return settings;
24
+ return normalizeCalendarSettings(
25
+ await getUserSetting(email, "calendar-settings"),
26
+ );
34
27
  } catch (error: any) {
35
28
  setResponseStatus(event, 500);
36
29
  return { error: error.message };
@@ -38,16 +31,19 @@ export const getSettings = defineEventHandler(async (event: H3Event) => {
38
31
  });
39
32
 
40
33
  export const getPublicSettings = defineEventHandler(async (_event: H3Event) => {
41
- const settings =
42
- ((await getSetting("calendar-settings")) as unknown as Settings | null) ||
43
- DEFAULT_SETTINGS;
44
- return settings;
34
+ return normalizeCalendarSettings(await getSetting("calendar-settings"));
45
35
  });
46
36
 
47
37
  export const updateSettings = defineEventHandler(async (event: H3Event) => {
48
38
  try {
49
39
  const email = await uEmail(event);
50
- const settings: Settings = await readBody(event);
40
+ const body = await readBody<unknown>(event);
41
+ const settings = normalizeCalendarSettings({
42
+ ...normalizeCalendarSettings(
43
+ await getUserSetting(email, "calendar-settings"),
44
+ ),
45
+ ...(body && typeof body === "object" ? body : {}),
46
+ });
51
47
  const settingsRecord = settings as unknown as Record<string, unknown>;
52
48
  await putUserSetting(email, "calendar-settings", settingsRecord);
53
49
  // Also write to global key so the public booking/settings page can read it
@@ -331,6 +331,7 @@ export interface Settings {
331
331
  bookingPageTitle: string;
332
332
  bookingPageDescription: string;
333
333
  defaultEventDuration: number; // minutes
334
+ weekStart: import("./calendar-week.js").CalendarWeekStart;
334
335
  }
335
336
 
336
337
  export type ApolloPersonResult = {
@@ -0,0 +1,64 @@
1
+ import type { TimeSlot } from "./api.js";
2
+
3
+ function timeToMinutes(value: string): number | null {
4
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value);
5
+ if (!match) return null;
6
+ const hours = Number(match[1]);
7
+ const minutes = Number(match[2]);
8
+ if (hours > 23 || minutes > 59) return null;
9
+ return hours * 60 + minutes;
10
+ }
11
+
12
+ /** Returns the saved windows in chronological order with adjacent overlaps merged. */
13
+ export function normalizeAvailabilitySlots(slots: TimeSlot[]): TimeSlot[] {
14
+ const valid = slots
15
+ .map((slot) => ({
16
+ slot,
17
+ start: timeToMinutes(slot.start),
18
+ end: timeToMinutes(slot.end),
19
+ }))
20
+ .filter(
21
+ (entry): entry is typeof entry & { start: number; end: number } =>
22
+ entry.start !== null && entry.end !== null && entry.start < entry.end,
23
+ )
24
+ .sort((a, b) => a.start - b.start || a.end - b.end);
25
+
26
+ const normalized: Array<{
27
+ slot: TimeSlot;
28
+ start: number;
29
+ end: number;
30
+ }> = [];
31
+ for (const entry of valid) {
32
+ const previous = normalized[normalized.length - 1];
33
+ if (previous && entry.start <= previous.end) {
34
+ const mergedEnd = Math.max(previous.end, entry.end);
35
+ previous.slot = {
36
+ ...previous.slot,
37
+ end: entry.end > previous.end ? entry.slot.end : previous.slot.end,
38
+ };
39
+ previous.end = mergedEnd;
40
+ continue;
41
+ }
42
+ normalized.push({ ...entry, slot: { ...entry.slot } });
43
+ }
44
+
45
+ return normalized.map(({ slot }) => slot);
46
+ }
47
+
48
+ export function availabilitySlotsOverlap(slots: TimeSlot[]): boolean {
49
+ const valid = slots
50
+ .map((slot) => ({
51
+ start: timeToMinutes(slot.start),
52
+ end: timeToMinutes(slot.end),
53
+ }))
54
+ .filter(
55
+ (entry): entry is { start: number; end: number } =>
56
+ entry.start !== null && entry.end !== null && entry.start < entry.end,
57
+ )
58
+ .sort((a, b) => a.start - b.start || a.end - b.end);
59
+
60
+ return valid.some((slot, index) => {
61
+ const previous = valid[index - 1];
62
+ return previous !== undefined && slot.start < previous.end;
63
+ });
64
+ }
@@ -0,0 +1,21 @@
1
+ export type CalendarWeekStart = "sunday" | "monday";
2
+
3
+ export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
4
+
5
+ export const DEFAULT_CALENDAR_WEEK_START: CalendarWeekStart = "sunday";
6
+
7
+ export function isCalendarWeekStart(
8
+ value: unknown,
9
+ ): value is CalendarWeekStart {
10
+ return value === "sunday" || value === "monday";
11
+ }
12
+
13
+ export function getWeekStartsOn(
14
+ weekStart: CalendarWeekStart | null | undefined,
15
+ ): 0 | 1 {
16
+ return weekStart === "monday" ? 1 : 0;
17
+ }
18
+
19
+ export function getWeekdayOrder(weekStartsOn: 0 | 1): Weekday[] {
20
+ return weekStartsOn === 1 ? [1, 2, 3, 4, 5, 6, 0] : [0, 1, 2, 3, 4, 5, 6];
21
+ }
@@ -0,0 +1,44 @@
1
+ import type { Settings } from "./api.js";
2
+ import {
3
+ DEFAULT_CALENDAR_WEEK_START,
4
+ isCalendarWeekStart,
5
+ } from "./calendar-week.js";
6
+
7
+ export const DEFAULT_SETTINGS: Settings = {
8
+ timezone: "America/New_York",
9
+ bookingPageTitle: "Book a Meeting",
10
+ bookingPageDescription: "Select a time that works for you.",
11
+ defaultEventDuration: 30,
12
+ weekStart: DEFAULT_CALENDAR_WEEK_START,
13
+ };
14
+
15
+ export function normalizeCalendarSettings(input: unknown): Settings {
16
+ const raw =
17
+ input && typeof input === "object"
18
+ ? (input as Partial<Settings>)
19
+ : ({} as Partial<Settings>);
20
+
21
+ return {
22
+ timezone:
23
+ typeof raw.timezone === "string"
24
+ ? raw.timezone
25
+ : DEFAULT_SETTINGS.timezone,
26
+ bookingPageTitle:
27
+ typeof raw.bookingPageTitle === "string"
28
+ ? raw.bookingPageTitle
29
+ : DEFAULT_SETTINGS.bookingPageTitle,
30
+ bookingPageDescription:
31
+ typeof raw.bookingPageDescription === "string"
32
+ ? raw.bookingPageDescription
33
+ : DEFAULT_SETTINGS.bookingPageDescription,
34
+ defaultEventDuration:
35
+ typeof raw.defaultEventDuration === "number" &&
36
+ Number.isFinite(raw.defaultEventDuration) &&
37
+ raw.defaultEventDuration > 0
38
+ ? raw.defaultEventDuration
39
+ : DEFAULT_SETTINGS.defaultEventDuration,
40
+ weekStart: isCalendarWeekStart(raw.weekStart)
41
+ ? raw.weekStart
42
+ : DEFAULT_SETTINGS.weekStart,
43
+ };
44
+ }
@@ -31,6 +31,8 @@ video sharing app. The agent and the UI share the same SQL data and actions.
31
31
  secrets/OAuth/runtime config and obvious placeholders.
32
32
  - Use actions for recording metadata, transcripts, cleanup, summaries, chapters,
33
33
  comments, spaces/folders, meetings, and sharing. Never bypass access helpers.
34
+ - Comment text accepts inline Markdown for emphasis, inline code, links, and
35
+ line breaks; headings and other block structures stay flattened in comments.
34
36
  - Recording start/stop/pause are UI gestures — browser capture needs user
35
37
  activation; navigate to the recording view instead of a server action.
36
38
  - Native transcript first; cloud transcription is fallback-only. Never hide a
@@ -20,10 +20,13 @@ import { nanoid } from "../server/lib/recordings.js";
20
20
 
21
21
  export default defineAction({
22
22
  description:
23
- "Add a comment to a recording at a specific video timestamp. For new threads, omit threadId/parentId. For replies, pass both.",
23
+ "Add a comment to a recording at a specific video timestamp. Comment text supports inline Markdown such as bold, italic, inline code, and links; headings are flattened in comment surfaces. For new threads, omit threadId/parentId. For replies, pass both.",
24
24
  schema: z.object({
25
25
  recordingId: z.string().describe("Recording ID"),
26
- content: z.string().min(1).describe("Comment text"),
26
+ content: z
27
+ .string()
28
+ .min(1)
29
+ .describe("Comment text; inline Markdown is supported, without headings"),
27
30
  videoTimestampMs: z
28
31
  .number()
29
32
  .int()
@@ -50,6 +50,10 @@ import {
50
50
  } from "../server/lib/resumable-session.js";
51
51
  import { resolveResumableUploadProvider } from "../server/lib/resumable-upload-provider.js";
52
52
  import { fetchS3ObjectByUrl } from "../server/lib/s3-upload-provider.js";
53
+ import {
54
+ clearSeekableRepairPending,
55
+ markSeekableRepairPending,
56
+ } from "../server/lib/seekable-media-state.js";
53
57
  import { isStreamingUploadDisabled } from "../server/lib/streaming-upload-mode.js";
54
58
  import {
55
59
  probeHasAudioStream,
@@ -59,7 +63,10 @@ import {
59
63
  requiresConfiguredVideoStorage,
60
64
  STORAGE_SETUP_REQUIRED_REASON,
61
65
  } from "../server/lib/video-storage.js";
62
- import { markRecordingSeekable } from "./lib/ensure-seekable-video.js";
66
+ import {
67
+ isRemoteProviderUrl,
68
+ markRecordingSeekable,
69
+ } from "./lib/ensure-seekable-video.js";
63
70
 
64
71
  // Recordings up to this size get their seekable rewrite applied inline during
65
72
  // finalize (we already hold the assembled bytes). Larger recordings are handed
@@ -398,6 +405,7 @@ async function persistPendingMediaVerification(params: {
398
405
  videoUrl: media.videoUrl,
399
406
  videoFormat: media.videoFormat,
400
407
  videoSizeBytes: media.videoSizeBytes,
408
+ mediaUpdatedAt: now,
401
409
  durationMs: media.finalDurationMs,
402
410
  width: media.finalWidth,
403
411
  height: media.finalHeight,
@@ -576,6 +584,7 @@ async function markRecordingReady(params: {
576
584
  videoUrl,
577
585
  videoFormat,
578
586
  videoSizeBytes,
587
+ mediaUpdatedAt: now,
579
588
  durationMs: finalDurationMs,
580
589
  width: finalWidth,
581
590
  height: finalHeight,
@@ -667,6 +676,12 @@ async function markRecordingReady(params: {
667
676
  if (seekableApplied) {
668
677
  // Uploaded bytes are already start-playable and seekable — remember it so
669
678
  // later reprocess sweeps skip this clip.
679
+ await clearSeekableRepairPending(id).catch((err) => {
680
+ console.warn("[finalize] failed to clear seekable repair marker", {
681
+ id,
682
+ error: err instanceof Error ? err.message : String(err),
683
+ });
684
+ });
670
685
  await markRecordingSeekable(id, videoUrl).catch((err) => {
671
686
  console.warn("[finalize] failed to write seekable marker", {
672
687
  id,
@@ -679,6 +694,17 @@ async function markRecordingReady(params: {
679
694
  // without a Cues index buffers on load and re-buffers on every seek. A
680
695
  // fresh self-dispatched request owns the repair so serverless runtimes do
681
696
  // not freeze it when this finalize request returns.
697
+ if (isRemoteProviderUrl(videoUrl)) {
698
+ await markSeekableRepairPending({
699
+ recordingId: id,
700
+ videoUrl,
701
+ }).catch((err) => {
702
+ console.warn("[finalize] failed to mark seekable repair pending", {
703
+ id,
704
+ error: err instanceof Error ? err.message : String(err),
705
+ });
706
+ });
707
+ }
682
708
  await dispatchPostFinalizeJob({
683
709
  recordingId: id,
684
710
  kind: "seekable",
@@ -1283,6 +1309,7 @@ export default defineAction({
1283
1309
  .set({
1284
1310
  status: "processing",
1285
1311
  uploadProgress: 100,
1312
+ mediaUpdatedAt: new Date().toISOString(),
1286
1313
  updatedAt: new Date().toISOString(),
1287
1314
  })
1288
1315
  .where(eq(schema.recordings.id, id));
@@ -1303,6 +1330,7 @@ export default defineAction({
1303
1330
  .set({
1304
1331
  status: "failed",
1305
1332
  failureReason,
1333
+ mediaUpdatedAt: now,
1306
1334
  updatedAt: now,
1307
1335
  })
1308
1336
  .where(eq(schema.recordings.id, id));
@@ -1623,6 +1651,7 @@ export default defineAction({
1623
1651
  hasAudio: finalHasAudio,
1624
1652
  hasCamera: finalHasCamera,
1625
1653
  uploadProgress: 0,
1654
+ mediaUpdatedAt: now,
1626
1655
  updatedAt: now,
1627
1656
  })
1628
1657
  .where(eq(schema.recordings.id, id));
@@ -1657,6 +1686,7 @@ export default defineAction({
1657
1686
  hasAudio: finalHasAudio,
1658
1687
  hasCamera: finalHasCamera,
1659
1688
  uploadProgress: 100,
1689
+ mediaUpdatedAt: now,
1660
1690
  updatedAt: now,
1661
1691
  })
1662
1692
  .where(eq(schema.recordings.id, id));
@@ -40,6 +40,7 @@ import {
40
40
  countRecordingViews,
41
41
  parseSpaceIds,
42
42
  } from "../server/lib/recordings.js";
43
+ import { isSeekableRepairPending } from "../server/lib/seekable-media-state.js";
43
44
  import { parseBrowserDiagnosticsRow } from "../shared/browser-diagnostics.js";
44
45
  import {
45
46
  CLIPS_BUILDER_CREDITS_STATE_KEY,
@@ -161,6 +162,7 @@ export default defineAction({
161
162
  cleanupStateRaw,
162
163
  builderCreditsRaw,
163
164
  verificationPending,
165
+ seekableRepairPending,
164
166
  viewCount,
165
167
  agentViewCount,
166
168
  ] = await Promise.all([
@@ -173,6 +175,12 @@ export default defineAction({
173
175
  recordingId: args.recordingId,
174
176
  recordingStatus: rec.status,
175
177
  }),
178
+ isSeekableRepairPending({
179
+ ownerEmail: rec.ownerEmail,
180
+ recordingId: args.recordingId,
181
+ recordingStatus: rec.status,
182
+ videoUrl: rec.videoUrl,
183
+ }),
176
184
  countRecordingViews(args.recordingId).catch(() => 0),
177
185
  countRecordingAgentViews(args.recordingId).catch(() => 0),
178
186
  ]);
@@ -336,6 +344,7 @@ export default defineAction({
336
344
  hasCamera: Boolean(rec.hasCamera),
337
345
  status: rec.status,
338
346
  verificationPending,
347
+ seekableRepairPending,
339
348
  uploadProgress: rec.uploadProgress,
340
349
  failureReason: rec.failureReason,
341
350
  // Don't leak the password to clients (especially to MCP hosts that
@@ -31,6 +31,7 @@ import { getDb, schema } from "../../server/db/index.js";
31
31
  import { queueBuilderMediaCompression } from "../../server/lib/builder-media-compression.js";
32
32
  import { deleteRecordingMediaObjects } from "../../server/lib/recording-media-cleanup.js";
33
33
  import { ownerEmailMatches } from "../../server/lib/recordings.js";
34
+ import { clearSeekableRepairPending } from "../../server/lib/seekable-media-state.js";
34
35
  import {
35
36
  makeSeekable,
36
37
  normalizeTimelineToMp4,
@@ -144,7 +145,7 @@ export async function fetchProviderBytes(
144
145
  * Ensure a single recording's stored media is seekable. Owner-scoped: pass the
145
146
  * resolved owner email so the DB lookup can only touch that owner's rows.
146
147
  */
147
- export async function ensureRecordingSeekable(params: {
148
+ async function ensureRecordingSeekableInternal(params: {
148
149
  recordingId: string;
149
150
  ownerEmail: string;
150
151
  force?: boolean;
@@ -281,6 +282,7 @@ export async function ensureRecordingSeekable(params: {
281
282
  videoUrl: upload.url,
282
283
  videoFormat: outputFormat,
283
284
  videoSizeBytes: seekable.bytes.byteLength,
285
+ mediaUpdatedAt: new Date().toISOString(),
284
286
  updatedAt: new Date().toISOString(),
285
287
  })
286
288
  .where(
@@ -340,3 +342,21 @@ export async function ensureRecordingSeekable(params: {
340
342
  videoUrl: upload.url,
341
343
  };
342
344
  }
345
+
346
+ export async function ensureRecordingSeekable(params: {
347
+ recordingId: string;
348
+ ownerEmail: string;
349
+ force?: boolean;
350
+ normalizeTimeline?: boolean;
351
+ }): Promise<EnsureSeekableResult> {
352
+ const result = await ensureRecordingSeekableInternal(params);
353
+ if (result.changed || result.status === "already-optimized") {
354
+ await clearSeekableRepairPending(params.recordingId).catch((err) => {
355
+ console.warn("[ensure-seekable-video] failed to clear pending marker", {
356
+ recordingId: params.recordingId,
357
+ error: err instanceof Error ? err.message : String(err),
358
+ });
359
+ });
360
+ }
361
+ return result;
362
+ }
@@ -20,10 +20,13 @@ import { nanoid } from "../server/lib/recordings.js";
20
20
 
21
21
  export default defineAction({
22
22
  description:
23
- "Reply to an existing comment. Looks up the thread and parent and delegates to add-comment.",
23
+ "Reply to an existing comment with inline Markdown text (without headings). Looks up the thread and parent and delegates to add-comment.",
24
24
  schema: z.object({
25
25
  commentId: z.string().describe("Comment ID to reply to"),
26
- content: z.string().min(1).describe("Reply text"),
26
+ content: z
27
+ .string()
28
+ .min(1)
29
+ .describe("Reply text; inline Markdown is supported, without headings"),
27
30
  authorName: z.string().optional(),
28
31
  }),
29
32
  run: async (args) => {
@@ -16,10 +16,17 @@ import { getDb, schema } from "../server/db/index.js";
16
16
  import { sameOwnerEmail } from "../server/lib/recordings.js";
17
17
 
18
18
  export default defineAction({
19
- description: "Update a comment's text. Only the comment author can edit it.",
19
+ description:
20
+ "Update a comment's text. Inline Markdown is supported without headings. Only the comment author can edit it.",
20
21
  schema: z.object({
21
22
  id: z.string().describe("Comment ID"),
22
- content: z.string().trim().min(1).describe("Updated comment text"),
23
+ content: z
24
+ .string()
25
+ .trim()
26
+ .min(1)
27
+ .describe(
28
+ "Updated comment text; inline Markdown is supported, without headings",
29
+ ),
23
30
  }),
24
31
  run: async (args) => {
25
32
  const userEmail = getRequestUserEmail();
@@ -1,7 +1,7 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import { IconClock, IconSearch, IconX } from "@tabler/icons-react";
3
- import React, { useEffect, useRef, useState } from "react";
4
- import { useNavigate } from "react-router";
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useNavigate, useSearchParams } from "react-router";
5
5
 
6
6
  import { msToClock } from "@/components/player/scrubber";
7
7
  import {
@@ -10,6 +10,10 @@ import {
10
10
  PopoverContent,
11
11
  } from "@/components/ui/popover";
12
12
  import { useRecordingSearch, type SearchHit } from "@/hooks/use-library";
13
+ import {
14
+ clearSearchFocusRequest,
15
+ hasSearchFocusRequest,
16
+ } from "@/lib/search-focus";
13
17
  import { cn, shortcutLabel } from "@/lib/utils";
14
18
 
15
19
  function highlight(
@@ -64,22 +68,33 @@ function matchLabel(hit: SearchHit, t: ReturnType<typeof useT>): string {
64
68
  export function SearchBar({ className, side = "right" }: SearchBarProps) {
65
69
  const t = useT();
66
70
  const navigate = useNavigate();
71
+ const [searchParams, setSearchParams] = useSearchParams();
67
72
  const [query, setQuery] = useState("");
68
73
  const [open, setOpen] = useState(false);
69
74
  const inputRef = useRef<HTMLInputElement>(null);
75
+ const routeRequestsFocus = hasSearchFocusRequest(searchParams);
70
76
 
71
77
  const { data, isFetching } = useRecordingSearch(query);
72
78
  const results: SearchHit[] = data?.results ?? [];
73
79
 
74
- // Cmd+K / Ctrl+K and "/" global shortcuts to focus search
80
+ const focusSearchInput = useCallback(() => {
81
+ inputRef.current?.focus();
82
+ inputRef.current?.select();
83
+ setOpen(true);
84
+ }, []);
85
+
86
+ useEffect(() => {
87
+ if (!routeRequestsFocus) return;
88
+ const frame = requestAnimationFrame(() => {
89
+ focusSearchInput();
90
+ setSearchParams(clearSearchFocusRequest, { replace: true });
91
+ });
92
+ return () => cancelAnimationFrame(frame);
93
+ }, [focusSearchInput, routeRequestsFocus, setSearchParams]);
94
+
95
+ // "/" is the inline search shortcut. Cmd+K belongs to the app command menu.
75
96
  useEffect(() => {
76
97
  const onKey = (e: KeyboardEvent) => {
77
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
78
- e.preventDefault();
79
- inputRef.current?.focus();
80
- inputRef.current?.select();
81
- setOpen(true);
82
- }
83
98
  if (
84
99
  e.key === "/" &&
85
100
  !e.metaKey &&
@@ -90,9 +105,7 @@ export function SearchBar({ className, side = "right" }: SearchBarProps) {
90
105
  !(e.target as HTMLElement)?.isContentEditable
91
106
  ) {
92
107
  e.preventDefault();
93
- inputRef.current?.focus();
94
- inputRef.current?.select();
95
- setOpen(true);
108
+ focusSearchInput();
96
109
  }
97
110
  if (e.key === "Escape") {
98
111
  setOpen(false);
@@ -101,7 +114,7 @@ export function SearchBar({ className, side = "right" }: SearchBarProps) {
101
114
  };
102
115
  window.addEventListener("keydown", onKey);
103
116
  return () => window.removeEventListener("keydown", onKey);
104
- }, []);
117
+ }, [focusSearchInput]);
105
118
 
106
119
  function pickResult(hit: SearchHit) {
107
120
  setOpen(false);