@agent-native/core 0.171.1 → 0.171.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +4 -5
  3. package/corpus/templates/calendar/actions/create-event.ts +11 -3
  4. package/corpus/templates/calendar/app/components/calendar/AddCalendarDialog.tsx +56 -51
  5. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +3 -0
  6. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +1 -1
  7. package/corpus/templates/calendar/app/components/calendar/PeopleSearchDialog.tsx +54 -51
  8. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +1 -1
  9. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -1
  10. package/corpus/templates/calendar/app/hooks/use-people.ts +23 -13
  11. package/corpus/templates/calendar/app/lib/event-layout.ts +21 -6
  12. package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
  13. package/corpus/templates/calendar/changelog/2026-08-24-overlapping-events-use-space-better.md +5 -0
  14. package/corpus/templates/calendar/server/lib/google-api.ts +9 -0
  15. package/corpus/templates/calendar/server/lib/google-calendar.ts +59 -2
  16. package/corpus/templates/calendar/server/lib/people-search.ts +43 -28
  17. package/corpus/templates/calendar/shared/api.ts +2 -0
  18. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +3 -3
  19. package/corpus/templates/mail/app/root.tsx +29 -0
  20. package/dist/client/DefaultSpinner.d.ts +3 -1
  21. package/dist/client/DefaultSpinner.js +2 -2
  22. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  23. package/dist/client/chat-first/apps-rail.js +11 -8
  24. package/dist/client/chat-first/copy.js +1 -0
  25. package/dist/client/chat-first/types.d.ts +1 -0
  26. package/dist/notifications/routes.d.ts +1 -1
  27. package/dist/resources/handlers.d.ts +1 -1
  28. package/dist/secrets/routes.d.ts +6 -6
  29. package/package.json +1 -1
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: 8585
34
+ - template files: 8586
@@ -2450,17 +2450,15 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2450
2450
  : "text-muted-foreground hover:bg-sidebar-accent/50",
2451
2451
  )}
2452
2452
  >
2453
- <button
2454
- type="button"
2455
- onClick={toggleDashOpen}
2453
+ <Link
2454
+ to="/dashboards"
2456
2455
  className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2 text-start"
2457
- aria-expanded={dashOpen}
2458
2456
  >
2459
2457
  <IconChartBar className="h-4 w-4 shrink-0" />
2460
2458
  <span className="min-w-0 flex-1 truncate">
2461
2459
  {t("navigation.dashboards")}
2462
2460
  </span>
2463
- </button>
2461
+ </Link>
2464
2462
  <SidebarSectionSettingsPopover
2465
2463
  label={t("navigation.dashboards")}
2466
2464
  sortMode={dashboardSortMode}
@@ -2477,6 +2475,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
2477
2475
  ? t("sidebar.collapseDashboards")
2478
2476
  : t("sidebar.expandDashboards")
2479
2477
  }
2478
+ aria-expanded={dashOpen}
2480
2479
  >
2481
2480
  <IconChevronDown
2482
2481
  className={cn(
@@ -216,10 +216,16 @@ export default defineAction({
216
216
  };
217
217
 
218
218
  let zoomMeetingLink: string | undefined;
219
+ let videoConferenceError: CalendarEvent["videoConferenceError"];
219
220
  if (args.addZoom) {
220
- const zoom = await prepareZoomMeetingPatch(email, calEvent);
221
- zoomMeetingLink = zoom.meetingLink;
222
- Object.assign(calEvent, zoom.patch);
221
+ try {
222
+ const zoom = await prepareZoomMeetingPatch(email, calEvent);
223
+ zoomMeetingLink = zoom.meetingLink;
224
+ Object.assign(calEvent, zoom.patch);
225
+ } catch (error) {
226
+ videoConferenceError = "zoom";
227
+ console.error("[create-event] Zoom meeting provisioning failed", error);
228
+ }
223
229
  }
224
230
 
225
231
  const result = await googleCalendar.createEvent(calEvent, {
@@ -238,6 +244,8 @@ export default defineAction({
238
244
  if (result.meetLink) calEvent.hangoutLink = result.meetLink;
239
245
  if (result.conferenceData) calEvent.conferenceData = result.conferenceData;
240
246
  if (zoomMeetingLink) calEvent.meetingLink = zoomMeetingLink;
247
+ if (videoConferenceError)
248
+ calEvent.videoConferenceError = videoConferenceError;
241
249
 
242
250
  try {
243
251
  emit(
@@ -1,4 +1,3 @@
1
- import { callAction } from "@agent-native/core/client/hooks";
2
1
  import { useT } from "@agent-native/core/client/i18n";
3
2
  import {
4
3
  IconSearch,
@@ -8,7 +7,7 @@ import {
8
7
  IconLink,
9
8
  IconCalendarPlus,
10
9
  } from "@tabler/icons-react";
11
- import { useState, useEffect, useRef, useCallback } from "react";
10
+ import { useState, useEffect, useMemo, useRef } from "react";
12
11
  import { toast } from "sonner";
13
12
 
14
13
  import { Button } from "@/components/ui/button";
@@ -31,17 +30,12 @@ import {
31
30
  useAddOverlayPerson,
32
31
  useRemoveOverlayPerson,
33
32
  } from "@/hooks/use-overlay-people";
34
-
35
- interface SearchResult {
36
- name: string;
37
- email: string;
38
- photoUrl?: string;
39
- }
40
-
41
- interface SearchResponse {
42
- results: SearchResult[];
43
- scopeRequired?: boolean;
44
- }
33
+ import {
34
+ filterPeopleResults,
35
+ mergePeopleResults,
36
+ usePeopleContacts,
37
+ usePeopleSearch,
38
+ } from "@/hooks/use-people";
45
39
 
46
40
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
47
41
  const URL_REGEX = /^(https?|webcal):\/\/.+/i;
@@ -67,7 +61,7 @@ export function AddCalendarDialog({
67
61
 
68
62
  return (
69
63
  <Dialog open={open} onOpenChange={onOpenChange}>
70
- <DialogContent className="sm:max-w-[440px] gap-0 p-0 top-[8%] translate-y-0">
64
+ <DialogContent className="sm:max-w-[440px] max-h-[calc(85vh-1rem)] gap-0 overflow-y-auto p-0 top-[15vh] !translate-y-0">
71
65
  <DialogHeader className="px-4 pt-4 pb-0">
72
66
  <DialogTitle className="text-base">
73
67
  {t("eventForm.addCalendar")}
@@ -89,7 +83,7 @@ export function AddCalendarDialog({
89
83
  </TabsList>
90
84
 
91
85
  <TabsContent value="people" className="mt-0">
92
- <PeopleTab onClose={() => onOpenChange(false)} />
86
+ <PeopleTab open={open} />
93
87
  </TabsContent>
94
88
 
95
89
  <TabsContent value="url" className="mt-0 px-4 pb-4 pt-3">
@@ -103,15 +97,12 @@ export function AddCalendarDialog({
103
97
 
104
98
  // ─── People tab ──────────────────────────────────────────────────────────────
105
99
 
106
- function PeopleTab({ onClose: _ }: { onClose: () => void }) {
100
+ function PeopleTab({ open }: { open: boolean }) {
107
101
  const t = useT();
108
102
  const [query, setQuery] = useState("");
109
- const [results, setResults] = useState<SearchResult[]>([]);
110
- const [scopeRequired, setScopeRequired] = useState(false);
111
- const [searching, setSearching] = useState(false);
103
+ const [searchQuery, setSearchQuery] = useState("");
112
104
  const [activeIndex, setActiveIndex] = useState(-1);
113
105
  const inputRef = useRef<HTMLInputElement>(null);
114
- const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
115
106
  const listRef = useRef<HTMLDivElement>(null);
116
107
  const shouldScrollActiveResultRef = useRef(false);
117
108
 
@@ -119,46 +110,57 @@ function PeopleTab({ onClose: _ }: { onClose: () => void }) {
119
110
  const overlayPeople = Array.isArray(rawOverlayPeople) ? rawOverlayPeople : [];
120
111
  const addPerson = useAddOverlayPerson();
121
112
  const removePerson = useRemoveOverlayPerson();
113
+ const contacts = usePeopleContacts("directory", open);
114
+ const directorySearch = usePeopleSearch(searchQuery, open, "directory");
122
115
 
123
- const overlayEmails = new Set(overlayPeople.map((p) => p.email));
124
- const selectableResults = results.filter((r) => !overlayEmails.has(r.email));
125
-
126
- const search = useCallback(async (q: string) => {
127
- setSearching(true);
128
- try {
129
- const data = await callAction<SearchResponse>(
130
- "search-people",
131
- q ? { q, scope: "directory" } : { scope: "directory" },
132
- { method: "GET" },
133
- );
134
- setResults(data.results ?? []);
135
- setScopeRequired(data.scopeRequired ?? false);
136
- } catch {
137
- // ignore
138
- } finally {
139
- setSearching(false);
140
- }
141
- }, []);
116
+ const overlayEmails = useMemo(
117
+ () => new Set(overlayPeople.map((p) => p.email.toLowerCase())),
118
+ [overlayPeople],
119
+ );
120
+
121
+ const results = useMemo(
122
+ () =>
123
+ filterPeopleResults(
124
+ mergePeopleResults(
125
+ contacts.data?.results,
126
+ directorySearch.data?.results,
127
+ ),
128
+ query,
129
+ new Set(),
130
+ ),
131
+ [contacts.data?.results, directorySearch.data?.results, query],
132
+ );
133
+ const selectableResults = results.filter(
134
+ (r) => !overlayEmails.has(r.email.toLowerCase()),
135
+ );
136
+
137
+ const searching =
138
+ contacts.isLoading ||
139
+ contacts.isFetching ||
140
+ directorySearch.isLoading ||
141
+ directorySearch.isFetching;
142
+ const scopeRequired = Boolean(
143
+ contacts.data?.scopeRequired || directorySearch.data?.scopeRequired,
144
+ );
142
145
 
143
146
  useEffect(() => {
144
- if (debounceRef.current) clearTimeout(debounceRef.current);
145
- debounceRef.current = setTimeout(() => search(query), query ? 300 : 0);
146
- return () => {
147
- if (debounceRef.current) clearTimeout(debounceRef.current);
148
- };
149
- }, [query, search]);
147
+ const timeout = window.setTimeout(
148
+ () => setSearchQuery(query),
149
+ query.trim() ? 300 : 0,
150
+ );
151
+ return () => window.clearTimeout(timeout);
152
+ }, [query]);
150
153
 
151
154
  useEffect(() => {
152
155
  setActiveIndex(results.length > 0 ? 0 : -1);
153
156
  }, [results]);
154
157
 
155
158
  useEffect(() => {
159
+ if (!open) return;
156
160
  setQuery("");
157
- setResults([]);
158
- setScopeRequired(false);
161
+ setSearchQuery("");
159
162
  setActiveIndex(-1);
160
- search("");
161
- }, [search]);
163
+ }, [open]);
162
164
 
163
165
  useEffect(() => {
164
166
  if (
@@ -198,7 +200,10 @@ function PeopleTab({ onClose: _ }: { onClose: () => void }) {
198
200
  return;
199
201
  }
200
202
  const trimmed = query.trim();
201
- if (EMAIL_REGEX.test(trimmed) && !overlayEmails.has(trimmed)) {
203
+ if (
204
+ EMAIL_REGEX.test(trimmed) &&
205
+ !overlayEmails.has(trimmed.toLowerCase())
206
+ ) {
202
207
  handleAdd(trimmed);
203
208
  setQuery("");
204
209
  }
@@ -229,7 +234,7 @@ function PeopleTab({ onClose: _ }: { onClose: () => void }) {
229
234
  className="max-h-48 overflow-y-auto border-t border-border"
230
235
  >
231
236
  {results.map((person) => {
232
- const alreadyAdded = overlayEmails.has(person.email);
237
+ const alreadyAdded = overlayEmails.has(person.email.toLowerCase());
233
238
  const selectableIdx = selectableResults.indexOf(person);
234
239
  const isActive = !alreadyAdded && selectableIdx === activeIndex;
235
240
  return (
@@ -856,6 +856,9 @@ export function CreateEventPopover({
856
856
  deletePersistedDraft(activeDraftId);
857
857
  onDraftCreated?.(activeDraftId);
858
858
  }
859
+ if (result?.videoConferenceError === "zoom") {
860
+ toast.error(t("eventForm.zoomAddFailed"));
861
+ }
859
862
  const eventId = result?.id;
860
863
  const undo = eventId
861
864
  ? () => {
@@ -297,7 +297,7 @@ const DayEventCard = memo(function DayEventCard({
297
297
  style={{
298
298
  ...posStyle,
299
299
  left: `calc(${li.left}% + ${li.indent}px)`,
300
- width: `calc(${li.width}% - ${li.indent * 2 + 2}px)`,
300
+ width: `calc(${li.width}% - ${li.indent + 2}px)`,
301
301
  zIndex:
302
302
  isBeingDragged && isDragging
303
303
  ? 100
@@ -1,4 +1,3 @@
1
- import { callAction } from "@agent-native/core/client/hooks";
2
1
  import { useT } from "@agent-native/core/client/i18n";
3
2
  import {
4
3
  IconSearch,
@@ -6,7 +5,7 @@ import {
6
5
  IconUserPlus,
7
6
  IconLoader2,
8
7
  } from "@tabler/icons-react";
9
- import { useState, useEffect, useRef, useCallback } from "react";
8
+ import { useState, useEffect, useMemo, useRef } from "react";
10
9
 
11
10
  import {
12
11
  Dialog,
@@ -19,17 +18,12 @@ import {
19
18
  useAddOverlayPerson,
20
19
  useRemoveOverlayPerson,
21
20
  } from "@/hooks/use-overlay-people";
22
-
23
- interface SearchResult {
24
- name: string;
25
- email: string;
26
- photoUrl?: string;
27
- }
28
-
29
- interface SearchResponse {
30
- results: SearchResult[];
31
- scopeRequired?: boolean;
32
- }
21
+ import {
22
+ filterPeopleResults,
23
+ mergePeopleResults,
24
+ usePeopleContacts,
25
+ usePeopleSearch,
26
+ } from "@/hooks/use-people";
33
27
 
34
28
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
35
29
 
@@ -44,12 +38,8 @@ export function PeopleSearchDialog({
44
38
  }: PeopleSearchDialogProps) {
45
39
  const t = useT();
46
40
  const [query, setQuery] = useState("");
47
- const [results, setResults] = useState<SearchResult[]>([]);
48
- const [scopeRequired, setScopeRequired] = useState(false);
49
- const [searching, setSearching] = useState(false);
41
+ const [searchQuery, setSearchQuery] = useState("");
50
42
  const [activeIndex, setActiveIndex] = useState(-1);
51
- const inputRef = useRef<HTMLInputElement>(null);
52
- const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
53
43
  const listRef = useRef<HTMLDivElement>(null);
54
44
  const shouldScrollActiveResultRef = useRef(false);
55
45
 
@@ -57,52 +47,61 @@ export function PeopleSearchDialog({
57
47
  const overlayPeople = Array.isArray(rawOverlayPeople) ? rawOverlayPeople : [];
58
48
  const addPerson = useAddOverlayPerson();
59
49
  const removePerson = useRemoveOverlayPerson();
50
+ const contacts = usePeopleContacts("directory", open);
51
+ const directorySearch = usePeopleSearch(searchQuery, open, "directory");
52
+
53
+ const overlayEmails = useMemo(
54
+ () => new Set(overlayPeople.map((p) => p.email.toLowerCase())),
55
+ [overlayPeople],
56
+ );
60
57
 
61
- const overlayEmails = new Set(overlayPeople.map((p) => p.email));
58
+ const results = useMemo(
59
+ () =>
60
+ filterPeopleResults(
61
+ mergePeopleResults(
62
+ contacts.data?.results,
63
+ directorySearch.data?.results,
64
+ ),
65
+ query,
66
+ new Set(),
67
+ ),
68
+ [contacts.data?.results, directorySearch.data?.results, query],
69
+ );
62
70
 
63
71
  // Selectable results (exclude already-added)
64
- const selectableResults = results.filter((r) => !overlayEmails.has(r.email));
72
+ const selectableResults = results.filter(
73
+ (r) => !overlayEmails.has(r.email.toLowerCase()),
74
+ );
65
75
 
66
- const search = useCallback(async (q: string) => {
67
- setSearching(true);
68
- try {
69
- const data = await callAction<SearchResponse>(
70
- "search-people",
71
- q ? { q, scope: "directory" } : { scope: "directory" },
72
- { method: "GET" },
73
- );
74
- setResults(data.results ?? []);
75
- setScopeRequired(data.scopeRequired ?? false);
76
- } catch {
77
- // ignore
78
- } finally {
79
- setSearching(false);
80
- }
81
- }, []);
76
+ const searching =
77
+ contacts.isLoading ||
78
+ contacts.isFetching ||
79
+ directorySearch.isLoading ||
80
+ directorySearch.isFetching;
81
+ const scopeRequired = Boolean(
82
+ contacts.data?.scopeRequired || directorySearch.data?.scopeRequired,
83
+ );
82
84
 
83
85
  useEffect(() => {
84
- if (debounceRef.current) clearTimeout(debounceRef.current);
85
- debounceRef.current = setTimeout(() => search(query), query ? 300 : 0);
86
- return () => {
87
- if (debounceRef.current) clearTimeout(debounceRef.current);
88
- };
89
- }, [query, search]);
86
+ const timeout = window.setTimeout(
87
+ () => setSearchQuery(query),
88
+ query.trim() ? 300 : 0,
89
+ );
90
+ return () => window.clearTimeout(timeout);
91
+ }, [query]);
90
92
 
91
93
  // Reset active index when results change
92
94
  useEffect(() => {
93
95
  setActiveIndex(results.length > 0 ? 0 : -1);
94
96
  }, [results]);
95
97
 
96
- // Load org contacts when dialog opens
97
98
  useEffect(() => {
98
99
  if (open) {
99
100
  setQuery("");
100
- setResults([]);
101
- setScopeRequired(false);
101
+ setSearchQuery("");
102
102
  setActiveIndex(-1);
103
- search("");
104
103
  }
105
- }, [open, search]);
104
+ }, [open]);
106
105
 
107
106
  function handleAdd(email: string, name?: string) {
108
107
  addPerson.mutate({ email, name });
@@ -145,7 +144,10 @@ export function PeopleSearchDialog({
145
144
  }
146
145
  // Otherwise, try adding as a typed email
147
146
  const trimmed = query.trim();
148
- if (EMAIL_REGEX.test(trimmed) && !overlayEmails.has(trimmed)) {
147
+ if (
148
+ EMAIL_REGEX.test(trimmed) &&
149
+ !overlayEmails.has(trimmed.toLowerCase())
150
+ ) {
149
151
  handleAdd(trimmed);
150
152
  setQuery("");
151
153
  }
@@ -154,7 +156,7 @@ export function PeopleSearchDialog({
154
156
 
155
157
  return (
156
158
  <Dialog open={open} onOpenChange={onOpenChange}>
157
- <DialogContent className="sm:max-w-[440px] gap-0 p-0">
159
+ <DialogContent className="sm:max-w-[440px] max-h-[calc(85vh-1rem)] gap-0 overflow-y-auto p-0 top-[15vh] !translate-y-0">
158
160
  <DialogHeader className="px-4 pt-4 pb-0">
159
161
  <DialogTitle className="text-base">
160
162
  {t("eventForm.people")}
@@ -165,7 +167,6 @@ export function PeopleSearchDialog({
165
167
  <div className="relative px-4 pt-3 pb-2">
166
168
  <IconSearch className="absolute left-7 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
167
169
  <input
168
- ref={inputRef}
169
170
  value={query}
170
171
  onChange={(e) => setQuery(e.target.value)}
171
172
  onKeyDown={handleKeyDown}
@@ -185,7 +186,9 @@ export function PeopleSearchDialog({
185
186
  className="max-h-48 overflow-y-auto border-t border-border"
186
187
  >
187
188
  {results.map((person) => {
188
- const alreadyAdded = overlayEmails.has(person.email);
189
+ const alreadyAdded = overlayEmails.has(
190
+ person.email.toLowerCase(),
191
+ );
189
192
  const selectableIdx = selectableResults.indexOf(person);
190
193
  const isActive = !alreadyAdded && selectableIdx === activeIndex;
191
194
  return (
@@ -367,7 +367,7 @@ const WeekEventCard = memo(function WeekEventCard({
367
367
  style={{
368
368
  ...style,
369
369
  left: `calc(${li.left}% + ${li.indent}px)`,
370
- width: `calc(${li.width}% - ${li.indent * 2 + 2}px)`,
370
+ width: `calc(${li.width}% - ${li.indent + 2}px)`,
371
371
  zIndex:
372
372
  isBeingDragged && isDragging
373
373
  ? 100
@@ -272,7 +272,10 @@ export function AppLayout({ children }: AppLayoutProps) {
272
272
 
273
273
  useEffect(() => {
274
274
  if (!hasAccounts) return;
275
- void prefetchPeopleContacts(queryClient);
275
+ void Promise.all([
276
+ prefetchPeopleContacts(queryClient),
277
+ prefetchPeopleContacts(queryClient, "directory"),
278
+ ]);
276
279
  }, [hasAccounts, queryClient]);
277
280
 
278
281
  useEffect(() => {
@@ -13,16 +13,18 @@ export interface PeopleSearchResponse {
13
13
  scopeRequired?: boolean;
14
14
  }
15
15
 
16
+ export type PeopleSearchScope = "all" | "directory";
17
+
16
18
  const PEOPLE_CONTACTS_STALE_TIME = 10 * 60_000;
17
19
  const PEOPLE_CONTACTS_GC_TIME = 30 * 60_000;
18
20
  const PEOPLE_SEARCH_STALE_TIME = 60_000;
19
21
  const PEOPLE_SEARCH_GC_TIME = 5 * 60_000;
20
22
 
21
- export const PEOPLE_CONTACTS_QUERY_KEY = [
22
- "action",
23
- "search-people",
24
- { scope: "all" },
25
- ] as const;
23
+ export function peopleContactsQueryKey(scope: PeopleSearchScope = "all") {
24
+ return ["action", "search-people", { scope }] as const;
25
+ }
26
+
27
+ export const PEOPLE_CONTACTS_QUERY_KEY = peopleContactsQueryKey();
26
28
 
27
29
  function sourceRank(source?: PeopleSearchResult["source"]) {
28
30
  switch (source) {
@@ -115,13 +117,14 @@ export function filterPeopleResults(
115
117
 
116
118
  export function prefetchPeopleContacts(
117
119
  queryClient: ReturnType<typeof useQueryClient>,
120
+ scope: PeopleSearchScope = "all",
118
121
  ) {
119
122
  return queryClient.prefetchQuery({
120
- queryKey: PEOPLE_CONTACTS_QUERY_KEY,
123
+ queryKey: peopleContactsQueryKey(scope),
121
124
  queryFn: () =>
122
125
  callAction<PeopleSearchResponse>(
123
126
  "search-people",
124
- { scope: "all" },
127
+ { scope },
125
128
  { method: "GET" },
126
129
  ),
127
130
  staleTime: PEOPLE_CONTACTS_STALE_TIME,
@@ -129,13 +132,16 @@ export function prefetchPeopleContacts(
129
132
  });
130
133
  }
131
134
 
132
- export function usePeopleContacts(enabled = true) {
135
+ export function usePeopleContacts(
136
+ scope: PeopleSearchScope = "all",
137
+ enabled = true,
138
+ ) {
133
139
  return useQuery({
134
- queryKey: PEOPLE_CONTACTS_QUERY_KEY,
140
+ queryKey: peopleContactsQueryKey(scope),
135
141
  queryFn: () =>
136
142
  callAction<PeopleSearchResponse>(
137
143
  "search-people",
138
- { scope: "all" },
144
+ { scope },
139
145
  { method: "GET" },
140
146
  ),
141
147
  enabled,
@@ -145,14 +151,18 @@ export function usePeopleContacts(enabled = true) {
145
151
  });
146
152
  }
147
153
 
148
- export function usePeopleSearch(query: string, enabled = true) {
154
+ export function usePeopleSearch(
155
+ query: string,
156
+ enabled = true,
157
+ scope: PeopleSearchScope = "all",
158
+ ) {
149
159
  const q = query.trim();
150
160
  return useQuery({
151
- queryKey: ["action", "search-people", { q, scope: "all" }],
161
+ queryKey: ["action", "search-people", { q, scope }],
152
162
  queryFn: () =>
153
163
  callAction<PeopleSearchResponse>(
154
164
  "search-people",
155
- { q, scope: "all" },
165
+ { q, scope },
156
166
  { method: "GET" },
157
167
  ),
158
168
  enabled: enabled && q.length > 0,
@@ -112,18 +112,33 @@ export function computeTimedEventLayout(
112
112
  }
113
113
 
114
114
  const totalCols = overlapLayers.length;
115
- // Give every overlap column a proportional slice of the day column so an
116
- // event's right edge stays visible instead of being covered by whichever
117
- // card renders on top of it.
118
- const width = 100 / totalCols;
115
+ const sameStartGroups = new Map<number, EventEntry[]>();
116
+ const sameStartIndexes = new Map<EventEntry, number>();
117
+ for (const entry of group) {
118
+ const entries = sameStartGroups.get(entry.bounds.start) ?? [];
119
+ sameStartIndexes.set(entry, entries.length);
120
+ entries.push(entry);
121
+ sameStartGroups.set(entry.bounds.start, entries);
122
+ }
119
123
 
120
124
  for (const entry of group) {
121
125
  const col = eventColumns.get(entry.event)!;
126
+ const sameStartEntries = sameStartGroups.get(entry.bounds.start)!;
127
+ const sameStartIndex = sameStartIndexes.get(entry)!;
128
+ const sameStartWidth = 100 / sameStartEntries.length;
129
+ const isGroupStart = entry.bounds.start === group[0].bounds.start;
130
+ const useStartLanes = sameStartEntries.length > 1;
131
+
132
+ // Later events sit over the earlier event instead of forcing both cards
133
+ // into equal columns. Events with the same start time still get lanes so
134
+ // their titles remain independently readable.
135
+ const left = useStartLanes ? sameStartIndex * sameStartWidth : 0;
136
+ const width = 100 - left;
122
137
 
123
138
  result.set(entry.event.id, {
124
- left: col * width,
139
+ left,
125
140
  width,
126
- indent: col * OVERLAP_INDENT_PX,
141
+ indent: isGroupStart && sameStartIndex === 0 ? 0 : OVERLAP_INDENT_PX,
127
142
  col,
128
143
  totalCols,
129
144
  stackOrder: stackOrder++,
@@ -896,6 +896,9 @@ export default function CalendarView() {
896
896
  createEvent.mutate(payload, {
897
897
  onSuccess: (result) => {
898
898
  discardedCommittingDraftsRef.current.delete(draftId);
899
+ if (result?.videoConferenceError === "zoom") {
900
+ toast.error(t("eventForm.zoomAddFailed"));
901
+ }
899
902
  const createdEventId = result?.id;
900
903
  if (createdEventId) {
901
904
  const undo = () => {
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-24
4
+ ---
5
+ Overlapping calendar events now use more of the available width while keeping same-time meetings readable.
@@ -130,6 +130,14 @@ export function isGoogleNotFoundError(error: unknown): boolean {
130
130
  );
131
131
  }
132
132
 
133
+ export function isGoogleEventAbsentError(error: unknown): boolean {
134
+ return (
135
+ isGoogleNotFoundError(error) ||
136
+ (error instanceof GoogleApiError && error.status === 410) ||
137
+ (error instanceof Error && /^Google API error \(410\):/.test(error.message))
138
+ );
139
+ }
140
+
133
141
  export async function googleFetch(
134
142
  url: string,
135
143
  accessToken: string,
@@ -364,6 +372,7 @@ export function calendarListEvents(
364
372
  timeMax?: string;
365
373
  q?: string;
366
374
  singleEvents?: boolean;
375
+ showDeleted?: boolean;
367
376
  orderBy?: string;
368
377
  maxResults?: number;
369
378
  pageToken?: string;
@@ -30,6 +30,7 @@ import {
30
30
  calendarDeleteEvent,
31
31
  calendarPatchEvent,
32
32
  calendarUpdateEvent,
33
+ isGoogleEventAbsentError,
33
34
  peopleGetProfile,
34
35
  } from "./google-api.js";
35
36
  import { getCalendarProviderApiRuntime } from "./provider-api.js";
@@ -1707,8 +1708,14 @@ export async function deleteEvent(
1707
1708
  return;
1708
1709
  }
1709
1710
 
1710
- const instanceStart = instance.start?.dateTime || instance.start?.date || "";
1711
- const isAllDay = !instance.start?.dateTime;
1711
+ const instanceStart =
1712
+ instance.originalStartTime?.dateTime ||
1713
+ instance.originalStartTime?.date ||
1714
+ instance.start?.dateTime ||
1715
+ instance.start?.date ||
1716
+ "";
1717
+ const isAllDay =
1718
+ !instance.originalStartTime?.dateTime && !instance.start?.dateTime;
1712
1719
 
1713
1720
  // Compute UNTIL value (day before this instance)
1714
1721
  const cutoff = new Date(instanceStart);
@@ -1748,6 +1755,56 @@ export async function deleteEvent(
1748
1755
  { recurrence: updatedRecurrence },
1749
1756
  { sendUpdates },
1750
1757
  );
1758
+
1759
+ // Truncating the master does not remove materialized exceptions after the
1760
+ // cutoff, so remove those exceptions as well.
1761
+ const instanceStartMs = Date.parse(instanceStart);
1762
+ const exceptionIds = new Set<string>([googleEventId]);
1763
+ let pageToken: string | undefined;
1764
+ do {
1765
+ const response = await calendarListEvents(client.accessToken, "primary", {
1766
+ singleEvents: false,
1767
+ showDeleted: true,
1768
+ maxResults: 2500,
1769
+ pageToken,
1770
+ });
1771
+ for (const event of response?.items || []) {
1772
+ if (event.recurringEventId !== recurringEventId) continue;
1773
+ const originalStart =
1774
+ event.originalStartTime?.dateTime ||
1775
+ event.originalStartTime?.date ||
1776
+ event.start?.dateTime ||
1777
+ event.start?.date;
1778
+ const originalStartMs =
1779
+ typeof originalStart === "string" ? Date.parse(originalStart) : NaN;
1780
+ if (
1781
+ event.id &&
1782
+ Number.isFinite(instanceStartMs) &&
1783
+ Number.isFinite(originalStartMs) &&
1784
+ originalStartMs >= instanceStartMs
1785
+ ) {
1786
+ exceptionIds.add(event.id);
1787
+ }
1788
+ }
1789
+ pageToken =
1790
+ typeof response?.nextPageToken === "string"
1791
+ ? response.nextPageToken
1792
+ : undefined;
1793
+ } while (pageToken);
1794
+
1795
+ for (const eventId of exceptionIds) {
1796
+ try {
1797
+ await calendarDeleteEvent(
1798
+ client.accessToken,
1799
+ "primary",
1800
+ eventId,
1801
+ sendUpdates,
1802
+ );
1803
+ } catch (error) {
1804
+ // A generated occurrence may already be gone once the master is trimmed.
1805
+ if (!isGoogleEventAbsentError(error)) throw error;
1806
+ }
1807
+ }
1751
1808
  }
1752
1809
 
1753
1810
  /**
@@ -36,6 +36,7 @@ interface ContactCacheEntry {
36
36
 
37
37
  const CONTACT_CACHE_TTL = 10 * 60 * 1000;
38
38
  const contactCache = new Map<string, ContactCacheEntry>();
39
+ const contactCacheLoads = new Map<string, Promise<ContactCacheEntry>>();
39
40
 
40
41
  const GENERIC_EMAIL_DOMAINS = new Set([
41
42
  "gmail.com",
@@ -255,36 +256,50 @@ async function loadCachedContactPeople(
255
256
  return cached;
256
257
  }
257
258
 
258
- const people = new Map<string, PersonResult>();
259
- let contactsLimited = false;
260
-
261
- await Promise.all(
262
- clients.map(async (client) => {
263
- try {
264
- const [connections, otherContacts] = await Promise.all([
265
- listConnectionPages(client.accessToken, "connections"),
266
- listConnectionPages(client.accessToken, "otherContacts"),
267
- ]);
268
-
269
- for (const person of extractPeople(connections, "contact")) {
270
- mergeCachedContact(people, person);
271
- }
272
- for (const person of extractPeople(otherContacts, "otherContact")) {
273
- mergeCachedContact(people, person);
259
+ const pending = contactCacheLoads.get(cacheKey);
260
+ if (pending) return pending;
261
+
262
+ const load = (async () => {
263
+ const people = new Map<string, PersonResult>();
264
+ let contactsLimited = false;
265
+
266
+ await Promise.all(
267
+ clients.map(async (client) => {
268
+ try {
269
+ const [connections, otherContacts] = await Promise.all([
270
+ listConnectionPages(client.accessToken, "connections"),
271
+ listConnectionPages(client.accessToken, "otherContacts"),
272
+ ]);
273
+
274
+ for (const person of extractPeople(connections, "contact")) {
275
+ mergeCachedContact(people, person);
276
+ }
277
+ for (const person of extractPeople(otherContacts, "otherContact")) {
278
+ mergeCachedContact(people, person);
279
+ }
280
+ } catch (error) {
281
+ if (permissionLimited(error)) contactsLimited = true;
274
282
  }
275
- } catch (error) {
276
- if (permissionLimited(error)) contactsLimited = true;
277
- }
278
- }),
279
- );
283
+ }),
284
+ );
280
285
 
281
- const entry: ContactCacheEntry = {
282
- people: Array.from(people.values()),
283
- contactsLimited,
284
- expiresAt: Date.now() + CONTACT_CACHE_TTL,
285
- };
286
- contactCache.set(cacheKey, entry);
287
- return entry;
286
+ const entry: ContactCacheEntry = {
287
+ people: Array.from(people.values()),
288
+ contactsLimited,
289
+ expiresAt: Date.now() + CONTACT_CACHE_TTL,
290
+ };
291
+ contactCache.set(cacheKey, entry);
292
+ return entry;
293
+ })();
294
+
295
+ contactCacheLoads.set(cacheKey, load);
296
+ try {
297
+ return await load;
298
+ } finally {
299
+ if (contactCacheLoads.get(cacheKey) === load) {
300
+ contactCacheLoads.delete(cacheKey);
301
+ }
302
+ }
288
303
  }
289
304
 
290
305
  export async function searchPeopleForUser(
@@ -66,6 +66,8 @@ export interface CalendarEvent {
66
66
  hangoutLink?: string; // Google Meet link
67
67
  /** Meeting URL stored in location/description for non-Google providers such as Zoom */
68
68
  meetingLink?: string;
69
+ /** Action-result warning when optional video conferencing could not be provisioned. */
70
+ videoConferenceError?: "zoom";
69
71
  conferenceData?: {
70
72
  entryPoints?: Array<{
71
73
  entryPointType: string;
@@ -103,9 +103,9 @@ export function PlaybackCommentOverlay({
103
103
  </p>
104
104
  <InlineMarkdown
105
105
  content={comment.content}
106
- className="line-clamp-3 text-sm leading-5 text-primary-foreground"
107
- linkClassName="text-primary-foreground underline decoration-primary-foreground/60 hover:decoration-primary-foreground"
108
- codeClassName="bg-primary-foreground/15 text-primary-foreground"
106
+ className="line-clamp-3 text-sm leading-5 text-background dark:text-foreground"
107
+ linkClassName="text-background underline decoration-background/60 hover:decoration-background dark:text-foreground dark:decoration-foreground/60 dark:hover:decoration-foreground"
108
+ codeClassName="bg-background/15 text-background dark:bg-foreground/15 dark:text-foreground"
109
109
  />
110
110
  </div>
111
111
  </div>
@@ -14,6 +14,11 @@ import {
14
14
  type LocaleCode,
15
15
  } from "@agent-native/core/client/i18n";
16
16
  import {
17
+ isDynamicImportFailureMessage,
18
+ recoverFromStaleChunkError,
19
+ } from "@agent-native/core/client/route-chunk-recovery";
20
+ import {
21
+ DefaultSpinner,
17
22
  ErrorReportActions,
18
23
  getThemeInitScript,
19
24
  } from "@agent-native/core/client/ui";
@@ -90,6 +95,7 @@ const MAIL_ERROR_COPY: Record<
90
95
  fallback: string;
91
96
  back: string;
92
97
  reload: string;
98
+ loading: string;
93
99
  sendFeedback: string;
94
100
  feedbackPlaceholder: string;
95
101
  openGitHubIssue: string;
@@ -100,6 +106,7 @@ const MAIL_ERROR_COPY: Record<
100
106
  fallback: "Something went wrong while loading Mail.",
101
107
  back: "Back",
102
108
  reload: "Reload",
109
+ loading: "Reloading Mail...",
103
110
  sendFeedback: "Send feedback",
104
111
  feedbackPlaceholder:
105
112
  "Describe what happened before this Mail error appeared.",
@@ -110,6 +117,7 @@ const MAIL_ERROR_COPY: Record<
110
117
  fallback: "加载 Mail 时出现问题。",
111
118
  back: "返回",
112
119
  reload: "重新加载",
120
+ loading: "正在重新加载 Mail...",
113
121
  sendFeedback: "发送反馈",
114
122
  feedbackPlaceholder: "描述此 Mail 错误出现前发生了什么。",
115
123
  openGitHubIssue: "打开 GitHub issue",
@@ -119,6 +127,7 @@ const MAIL_ERROR_COPY: Record<
119
127
  fallback: "載入 Mail 時發生問題。",
120
128
  back: "返回",
121
129
  reload: "重新載入",
130
+ loading: "正在重新載入 Mail...",
122
131
  sendFeedback: "傳送意見回饋",
123
132
  feedbackPlaceholder: "描述此 Mail 錯誤出現前發生了什麼。",
124
133
  openGitHubIssue: "開啟 GitHub issue",
@@ -128,6 +137,7 @@ const MAIL_ERROR_COPY: Record<
128
137
  fallback: "Algo salió mal al cargar Mail.",
129
138
  back: "Atrás",
130
139
  reload: "Recargar",
140
+ loading: "Recargando Mail...",
131
141
  sendFeedback: "Enviar comentarios",
132
142
  feedbackPlaceholder:
133
143
  "Describe qué pasó antes de que apareciera este error de Mail.",
@@ -138,6 +148,7 @@ const MAIL_ERROR_COPY: Record<
138
148
  fallback: "Un problème est survenu lors du chargement de Mail.",
139
149
  back: "Retour",
140
150
  reload: "Recharger",
151
+ loading: "Rechargement de Mail...",
141
152
  sendFeedback: "Envoyer un retour",
142
153
  feedbackPlaceholder:
143
154
  "Décrivez ce qui s'est passé avant cette erreur de Mail.",
@@ -148,6 +159,7 @@ const MAIL_ERROR_COPY: Record<
148
159
  fallback: "Beim Laden von Mail ist ein Fehler aufgetreten.",
149
160
  back: "Zurück",
150
161
  reload: "Neu laden",
162
+ loading: "Mail wird neu geladen...",
151
163
  sendFeedback: "Feedback senden",
152
164
  feedbackPlaceholder:
153
165
  "Beschreiben Sie, was vor diesem Mail-Fehler passiert ist.",
@@ -158,6 +170,7 @@ const MAIL_ERROR_COPY: Record<
158
170
  fallback: "Mail の読み込み中に問題が発生しました。",
159
171
  back: "戻る",
160
172
  reload: "再読み込み",
173
+ loading: "Mail を再読み込み中...",
161
174
  sendFeedback: "フィードバックを送信",
162
175
  feedbackPlaceholder:
163
176
  "この Mail エラーの直前に起きたことを説明してください。",
@@ -168,6 +181,7 @@ const MAIL_ERROR_COPY: Record<
168
181
  fallback: "Mail을 불러오는 중 문제가 발생했습니다.",
169
182
  back: "뒤로",
170
183
  reload: "새로고침",
184
+ loading: "Mail 새로고침 중...",
171
185
  sendFeedback: "피드백 보내기",
172
186
  feedbackPlaceholder:
173
187
  "이 Mail 오류가 나타나기 전에 무슨 일이 있었는지 적어 주세요.",
@@ -178,6 +192,7 @@ const MAIL_ERROR_COPY: Record<
178
192
  fallback: "Algo deu errado ao carregar o Mail.",
179
193
  back: "Voltar",
180
194
  reload: "Recarregar",
195
+ loading: "Recarregando o Mail...",
181
196
  sendFeedback: "Enviar feedback",
182
197
  feedbackPlaceholder:
183
198
  "Descreva o que aconteceu antes deste erro do Mail aparecer.",
@@ -188,6 +203,7 @@ const MAIL_ERROR_COPY: Record<
188
203
  fallback: "Mail लोड करते समय कुछ गलत हुआ।",
189
204
  back: "वापस",
190
205
  reload: "रीलोड",
206
+ loading: "Mail रीलोड हो रहा है...",
191
207
  sendFeedback: "फ़ीडबैक भेजें",
192
208
  feedbackPlaceholder: "इस Mail त्रुटि से पहले क्या हुआ, उसका वर्णन करें।",
193
209
  openGitHubIssue: "GitHub issue खोलें",
@@ -197,6 +213,7 @@ const MAIL_ERROR_COPY: Record<
197
213
  fallback: "حدث خطأ أثناء تحميل Mail.",
198
214
  back: "رجوع",
199
215
  reload: "إعادة التحميل",
216
+ loading: "جارٍ إعادة تحميل Mail...",
200
217
  sendFeedback: "إرسال الملاحظات",
201
218
  feedbackPlaceholder: "صف ما حدث قبل ظهور خطأ Mail هذا.",
202
219
  openGitHubIssue: "فتح مشكلة في GitHub",
@@ -469,6 +486,18 @@ export function ErrorBoundary() {
469
486
  const copy =
470
487
  MAIL_ERROR_COPY[activeErrorLocale()] ?? MAIL_ERROR_COPY[DEFAULT_LOCALE];
471
488
  const message = routeErrorMessage(error, copy.fallback);
489
+ const staleChunk = isDynamicImportFailureMessage(message);
490
+ const [recovering, setRecovering] = useState(staleChunk);
491
+
492
+ useEffect(() => {
493
+ if (!staleChunk) {
494
+ setRecovering(false);
495
+ return;
496
+ }
497
+ if (!recoverFromStaleChunkError(error)) setRecovering(false);
498
+ }, [error, staleChunk]);
499
+
500
+ if (recovering) return <DefaultSpinner ariaLabel={copy.loading} />;
472
501
 
473
502
  return (
474
503
  <div className="flex min-h-screen items-center justify-center bg-background px-6 text-foreground">
@@ -1 +1,3 @@
1
- export declare function DefaultSpinner(): import("react").JSX.Element;
1
+ export declare function DefaultSpinner({ ariaLabel, }: {
2
+ ariaLabel?: string;
3
+ }): import("react").JSX.Element;
@@ -23,7 +23,7 @@ function isDevelopmentBuild() {
23
23
  return false;
24
24
  return viteEnv?.DEV === true;
25
25
  }
26
- export function DefaultSpinner() {
26
+ export function DefaultSpinner({ ariaLabel = "Loading", }) {
27
27
  const [showStallHint, setShowStallHint] = useState(false);
28
28
  useEffect(() => {
29
29
  setShowStallHint(isDevelopmentBuild());
@@ -36,7 +36,7 @@ export function DefaultSpinner() {
36
36
  gap: 16,
37
37
  height: "100vh",
38
38
  width: "100%",
39
- }, children: [_jsx("svg", { role: "status", "aria-label": "Loading", width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", style: { animation: "an-spin 1s linear infinite", opacity: 0.7 }, children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }), showStallHint && (_jsx("p", { className: "an-stall-hint", children: "Still loading. A first run compiles dependencies and can take a minute \u2014 if it does not finish, check the terminal running the dev server for errors." })), _jsx("style", { children: `
39
+ }, children: [_jsx("svg", { role: "status", "aria-label": ariaLabel, width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", style: { animation: "an-spin 1s linear infinite", opacity: 0.7 }, children: _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) }), showStallHint && (_jsx("p", { className: "an-stall-hint", children: "Still loading. A first run compiles dependencies and can take a minute \u2014 if it does not finish, check the terminal running the dev server for errors." })), _jsx("style", { children: `
40
40
  @keyframes an-spin { to { transform: rotate(360deg) } }
41
41
  ${showStallHint
42
42
  ? `
@@ -1,3 +1,3 @@
1
1
  import type { ChatFirstAppRailProps } from "./types.js";
2
2
  export declare const CHAT_FIRST_APP_RAIL_SHOW_ALL_STORAGE_KEY = "agent-native:chat-first-app-rail-show-all:v1";
3
- export declare const ChatFirstAppsRail: import("react").MemoExoticComponent<({ apps, defaultAppIds, activeAppId, loading, error, collapsed, layout: controlledLayout, onLayoutChange, onLayoutError, onRetry, onOpenApp, onRemoveApp, onOpenAllApps, onCreateApp, createAppTrigger, renderIcon, copy, }: ChatFirstAppRailProps) => import("react").JSX.Element>;
3
+ export declare const ChatFirstAppsRail: import("react").MemoExoticComponent<({ apps, defaultAppIds, activeAppId, loading, error, collapsed, layout: controlledLayout, onLayoutChange, onLayoutError, onRetry, onOpenApp, onReloadApp, onRemoveApp, onOpenAllApps, onCreateApp, createAppTrigger, renderIcon, copy, }: ChatFirstAppRailProps) => import("react").JSX.Element>;
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, Skeleton, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@agent-native/toolkit/ui";
3
- import { IconApps, IconChevronDown, IconChevronUp, IconPin, IconPlus, IconTrash, } from "@tabler/icons-react";
3
+ import { IconApps, IconChevronDown, IconChevronUp, IconPin, IconPlus, IconRefresh, IconTrash, } from "@tabler/icons-react";
4
4
  import { memo, useEffect, useMemo, useState } from "react";
5
5
  import { CHAT_FIRST_DEFAULT_APP_IDS, orderChatFirstAppIds, readChatFirstAppLayout, writeChatFirstAppLayout, } from "../chat-first.js";
6
6
  import { cn } from "../utils.js";
@@ -33,7 +33,10 @@ function ChatFirstRailAppIcon({ app, activeAppId, renderIcon, }) {
33
33
  const isInactive = activeAppId !== undefined && !isActive;
34
34
  return (_jsx("span", { "data-chat-first-app-icon": true, className: cn("transition-[filter]", isInactive && "grayscale"), children: renderIcon(app, { isActive, isInactive }) }));
35
35
  }
36
- function AppRows({ apps, defaultAppIds, activeAppId, layout, onDragStart, onDrop, onDragEnd, onOpenApp, onRemoveApp, onTogglePinned, onMove, renderIcon, copy, }) {
36
+ function AppContextMenuContent({ app, copy, index, onMove, onReloadApp, onRemoveApp, onTogglePinned, pinned, total, }) {
37
+ return (_jsxs(ContextMenuContent, { children: [_jsxs(ContextMenuItem, { onSelect: () => onTogglePinned(app.id), children: [_jsx(IconPin, { size: 14, "aria-hidden": "true" }), pinned ? copy("removePinned") : copy("pinTop")] }), onReloadApp ? (_jsxs(ContextMenuItem, { onSelect: () => onReloadApp(app), children: [_jsx(IconRefresh, { size: 14, "aria-hidden": "true" }), copy("reloadApp")] })) : null, _jsx(ContextMenuSeparator, {}), _jsxs(ContextMenuItem, { disabled: index === 0, onSelect: () => onMove(app.id, -1), children: [_jsx(IconChevronUp, { size: 14, "aria-hidden": "true" }), copy("moveUp")] }), _jsxs(ContextMenuItem, { disabled: index === total - 1, onSelect: () => onMove(app.id, 1), children: [_jsx(IconChevronDown, { size: 14, "aria-hidden": "true" }), copy("moveDown")] }), onRemoveApp ? (_jsxs(_Fragment, { children: [_jsx(ContextMenuSeparator, {}), _jsxs(ContextMenuItem, { onSelect: () => onRemoveApp(app), className: "text-destructive focus:text-destructive", children: [_jsx(IconTrash, { size: 14, "aria-hidden": "true" }), copy("removeApp")] })] })) : null] }));
38
+ }
39
+ function AppRows({ apps, defaultAppIds, activeAppId, layout, onDragStart, onDrop, onDragEnd, onOpenApp, onReloadApp, onRemoveApp, onTogglePinned, onMove, renderIcon, copy, }) {
37
40
  const orderedIds = orderChatFirstAppIds(apps.map((app) => app.id), layout, defaultAppIds);
38
41
  const appsById = new Map(apps.map((app) => [app.id, app]));
39
42
  const orderedApps = orderedIds
@@ -61,10 +64,10 @@ function AppRows({ apps, defaultAppIds, activeAppId, layout, onDragStart, onDrop
61
64
  }
62
65
  }, "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown", "aria-label": copy("openApp", { name: app.name }), children: [_jsx(ChatFirstRailAppIcon, { app: app, activeAppId: activeAppId, renderIcon: renderIcon }), _jsx("span", { className: "truncate", children: app.name })] }), _jsx("button", { type: "button", className: cn("flex size-6 shrink-0 items-center justify-center rounded text-sidebar-foreground/45 opacity-0 hover:bg-sidebar-accent hover:text-sidebar-foreground group-hover:opacity-100 focus-visible:opacity-100", pinned && "text-sidebar-foreground/70 opacity-100"), "aria-label": copy(pinned ? "unpinApp" : "pinApp", {
63
66
  name: app.name,
64
- }), "aria-pressed": pinned, onClick: () => onTogglePinned(app.id), children: _jsx(IconPin, { size: 13, strokeWidth: pinned ? 2.2 : 1.6, "aria-hidden": "true" }) }), _jsx("span", { className: "sr-only", children: `${index + 1} of ${orderedApps.length}` })] }) }), _jsxs(ContextMenuContent, { children: [_jsxs(ContextMenuItem, { onSelect: () => onTogglePinned(app.id), children: [_jsx(IconPin, { size: 14, "aria-hidden": "true" }), pinned ? copy("removePinned") : copy("pinTop")] }), _jsx(ContextMenuSeparator, {}), _jsxs(ContextMenuItem, { disabled: index === 0, onSelect: () => onMove(app.id, -1), children: [_jsx(IconChevronUp, { size: 14, "aria-hidden": "true" }), copy("moveUp")] }), _jsxs(ContextMenuItem, { disabled: index === orderedApps.length - 1, onSelect: () => onMove(app.id, 1), children: [_jsx(IconChevronDown, { size: 14, "aria-hidden": "true" }), copy("moveDown")] }), onRemoveApp ? (_jsxs(_Fragment, { children: [_jsx(ContextMenuSeparator, {}), _jsxs(ContextMenuItem, { onSelect: () => onRemoveApp(app), className: "text-destructive focus:text-destructive", children: [_jsx(IconTrash, { size: 14, "aria-hidden": "true" }), copy("removeApp")] })] })) : null] })] }, app.id));
67
+ }), "aria-pressed": pinned, onClick: () => onTogglePinned(app.id), children: _jsx(IconPin, { size: 13, strokeWidth: pinned ? 2.2 : 1.6, "aria-hidden": "true" }) }), _jsx("span", { className: "sr-only", children: `${index + 1} of ${orderedApps.length}` })] }) }), _jsx(AppContextMenuContent, { app: app, copy: copy, index: index, onMove: onMove, onReloadApp: onReloadApp, onRemoveApp: onRemoveApp, onTogglePinned: onTogglePinned, pinned: pinned, total: orderedApps.length })] }, app.id));
65
68
  }) }));
66
69
  }
67
- export const ChatFirstAppsRail = memo(function ChatFirstAppsRail({ apps, defaultAppIds, activeAppId, loading = false, error, collapsed = false, layout: controlledLayout, onLayoutChange, onLayoutError, onRetry, onOpenApp, onRemoveApp, onOpenAllApps, onCreateApp, createAppTrigger, renderIcon, copy = defaultChatFirstCopy, }) {
70
+ export const ChatFirstAppsRail = memo(function ChatFirstAppsRail({ apps, defaultAppIds, activeAppId, loading = false, error, collapsed = false, layout: controlledLayout, onLayoutChange, onLayoutError, onRetry, onOpenApp, onReloadApp, onRemoveApp, onOpenAllApps, onCreateApp, createAppTrigger, renderIcon, copy = defaultChatFirstCopy, }) {
68
71
  const [localLayout, setLocalLayout] = useState(() => readChatFirstAppLayout());
69
72
  const layout = controlledLayout ?? localLayout;
70
73
  const [draggedAppId, setDraggedAppId] = useState(null);
@@ -135,9 +138,9 @@ export const ChatFirstAppsRail = memo(function ChatFirstAppsRail({ apps, default
135
138
  if (collapsed) {
136
139
  return (_jsx(TooltipProvider, { children: _jsxs("section", { "data-chat-first-apps-rail": true, className: "flex flex-col items-center gap-1 px-1.5 pt-2", "aria-label": copy("workspaceApps"), children: [loading && apps.length === 0
137
140
  ? [0, 1, 2].map((index) => (_jsx(Skeleton, { className: "size-9 rounded-md" }, index)))
138
- : visibleApps.map((app) => (_jsxs(Tooltip, { delayDuration: 0, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-chat-first-app": true, "data-app-id": app.id, className: cn("flex size-9 items-center justify-center rounded-md", activeAppId === app.id
139
- ? "bg-sidebar-accent text-sidebar-accent-foreground"
140
- : "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"), onClick: () => onOpenApp(app), "aria-label": copy("openApp", { name: app.name }), children: _jsx(ChatFirstRailAppIcon, { app: app, activeAppId: activeAppId, renderIcon: renderIcon }) }) }), _jsx(TooltipContent, { side: "right", children: app.name })] }, app.id))), onOpenAllApps ? (_jsxs(Tooltip, { delayDuration: 0, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-chat-first-all-apps": true, className: "flex size-9 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", onClick: onOpenAllApps, "aria-label": copy("allApps"), children: _jsx(IconApps, { size: 16, "aria-hidden": "true" }) }) }), _jsx(TooltipContent, { side: "right", children: copy("allApps") })] })) : null, error ? (_jsx("span", { className: "size-1.5 rounded-full bg-destructive", title: error, "aria-label": error })) : null] }) }));
141
+ : visibleApps.map((app) => (_jsxs(ContextMenu, { children: [_jsxs(Tooltip, { delayDuration: 0, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(ContextMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-chat-first-app": true, "data-app-id": app.id, className: cn("flex size-9 items-center justify-center rounded-md", activeAppId === app.id
142
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
143
+ : "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"), onClick: () => onOpenApp(app), "aria-label": copy("openApp", { name: app.name }), children: _jsx(ChatFirstRailAppIcon, { app: app, activeAppId: activeAppId, renderIcon: renderIcon }) }) }) }), _jsx(TooltipContent, { side: "right", children: app.name })] }), _jsx(AppContextMenuContent, { app: app, copy: copy, index: orderedApps.indexOf(app), onMove: moveApp, onReloadApp: onReloadApp, onRemoveApp: onRemoveApp, onTogglePinned: togglePinned, pinned: layout.pinnedIds.includes(app.id), total: orderedApps.length })] }, app.id))), onOpenAllApps ? (_jsxs(Tooltip, { delayDuration: 0, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-chat-first-all-apps": true, className: "flex size-9 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", onClick: onOpenAllApps, "aria-label": copy("allApps"), children: _jsx(IconApps, { size: 16, "aria-hidden": "true" }) }) }), _jsx(TooltipContent, { side: "right", children: copy("allApps") })] })) : null, error ? (_jsx("span", { className: "size-1.5 rounded-full bg-destructive", title: error, "aria-label": error })) : null] }) }));
141
144
  }
142
- return (_jsxs("section", { "data-chat-first-apps-rail": true, className: "mt-3 px-2 pb-2 pt-2", "aria-label": copy("workspaceApps"), children: [_jsxs("div", { className: "mb-1 flex items-center gap-1.5 px-2 text-[11px] font-medium text-sidebar-foreground/50", children: [_jsx("span", { children: copy("workspaceApps") }), _jsx("span", { className: "ml-auto", children: createTrigger })] }), loading && apps.length === 0 ? (_jsx("div", { className: "space-y-0.5 px-2", children: [0, 1, 2].map((index) => (_jsx(Skeleton, { className: "h-8 w-full rounded-md" }, index))) })) : apps.length === 0 ? (_jsxs("div", { className: "px-2", children: [_jsx("p", { className: "text-xs text-sidebar-foreground/55", children: copy("noWorkspaceApps") }), createAppTrigger ? (createTrigger) : onCreateApp ? (_jsxs("button", { type: "button", className: "mt-1 inline-flex h-6 items-center gap-1.5 rounded-md border border-sidebar-border px-2 text-xs text-sidebar-foreground/75 hover:bg-sidebar-accent hover:text-sidebar-foreground", onClick: onCreateApp, children: [_jsx(IconPlus, { size: 13, "aria-hidden": "true" }), copy("createApp")] })) : (createTrigger)] })) : (_jsx(AppRows, { apps: visibleApps, defaultAppIds: defaultAppIds, activeAppId: activeAppId, layout: layout, onDragStart: setDraggedAppId, onDrop: reorderApps, onDragEnd: () => setDraggedAppId(null), onOpenApp: onOpenApp, onRemoveApp: onRemoveApp, onTogglePinned: togglePinned, onMove: moveApp, renderIcon: renderIcon, copy: copy })), hasMoreApps || showAllApps ? (_jsxs("button", { type: "button", className: "mt-0.5 flex h-8 w-full items-center gap-2 rounded-md px-2 text-xs text-sidebar-foreground/50 hover:bg-sidebar-accent hover:text-sidebar-foreground", onClick: () => setShowAllApps((value) => !value), children: [showAllApps ? (_jsx(IconChevronUp, { size: 14, "aria-hidden": "true" })) : (_jsx(IconChevronDown, { size: 14, "aria-hidden": "true" })), showAllApps ? copy("showLess") : copy("showMore")] })) : null, onOpenAllApps ? (_jsx("div", { className: "mt-1 border-t border-sidebar-border/60 pt-1", children: _jsxs("button", { type: "button", "data-chat-first-all-apps": true, className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-xs text-sidebar-foreground/55 hover:bg-sidebar-accent hover:text-sidebar-foreground", onClick: onOpenAllApps, children: [_jsx(IconApps, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: copy("allApps") })] }) })) : null, error ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "mt-1 flex items-center gap-1 px-2 text-[10px] text-destructive/80", onClick: onRetry, "aria-label": error, children: [_jsx("span", { className: "size-1.5 rounded-full bg-destructive" }), onRetry ? copy("retry") : copy("appsLoadError")] }) }), _jsx(TooltipContent, { side: "right", children: error })] })) : null] }));
145
+ return (_jsxs("section", { "data-chat-first-apps-rail": true, className: "mt-3 px-2 pb-2 pt-2", "aria-label": copy("workspaceApps"), children: [_jsxs("div", { className: "mb-1 flex items-center gap-1.5 px-2 text-[11px] font-medium text-sidebar-foreground/50", children: [_jsx("span", { children: copy("workspaceApps") }), _jsx("span", { className: "ml-auto", children: createTrigger })] }), loading && apps.length === 0 ? (_jsx("div", { className: "space-y-0.5 px-2", children: [0, 1, 2].map((index) => (_jsx(Skeleton, { className: "h-8 w-full rounded-md" }, index))) })) : apps.length === 0 ? (_jsxs("div", { className: "px-2", children: [_jsx("p", { className: "text-xs text-sidebar-foreground/55", children: copy("noWorkspaceApps") }), createAppTrigger ? (createTrigger) : onCreateApp ? (_jsxs("button", { type: "button", className: "mt-1 inline-flex h-6 items-center gap-1.5 rounded-md border border-sidebar-border px-2 text-xs text-sidebar-foreground/75 hover:bg-sidebar-accent hover:text-sidebar-foreground", onClick: onCreateApp, children: [_jsx(IconPlus, { size: 13, "aria-hidden": "true" }), copy("createApp")] })) : (createTrigger)] })) : (_jsx(AppRows, { apps: visibleApps, defaultAppIds: defaultAppIds, activeAppId: activeAppId, layout: layout, onDragStart: setDraggedAppId, onDrop: reorderApps, onDragEnd: () => setDraggedAppId(null), onOpenApp: onOpenApp, onReloadApp: onReloadApp, onRemoveApp: onRemoveApp, onTogglePinned: togglePinned, onMove: moveApp, renderIcon: renderIcon, copy: copy })), hasMoreApps || showAllApps ? (_jsxs("button", { type: "button", className: "mt-0.5 flex h-8 w-full items-center gap-2 rounded-md px-2 text-xs text-sidebar-foreground/50 hover:bg-sidebar-accent hover:text-sidebar-foreground", onClick: () => setShowAllApps((value) => !value), children: [showAllApps ? (_jsx(IconChevronUp, { size: 14, "aria-hidden": "true" })) : (_jsx(IconChevronDown, { size: 14, "aria-hidden": "true" })), showAllApps ? copy("showLess") : copy("showMore")] })) : null, onOpenAllApps ? (_jsx("div", { className: "mt-1 border-t border-sidebar-border/60 pt-1", children: _jsxs("button", { type: "button", "data-chat-first-all-apps": true, className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-xs text-sidebar-foreground/55 hover:bg-sidebar-accent hover:text-sidebar-foreground", onClick: onOpenAllApps, children: [_jsx(IconApps, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: copy("allApps") })] }) })) : null, error ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "mt-1 flex items-center gap-1 px-2 text-[10px] text-destructive/80", onClick: onRetry, "aria-label": error, children: [_jsx("span", { className: "size-1.5 rounded-full bg-destructive" }), onRetry ? copy("retry") : copy("appsLoadError")] }) }), _jsx(TooltipContent, { side: "right", children: error })] })) : null] }));
143
146
  });
@@ -14,6 +14,7 @@ const DEFAULT_COPY = {
14
14
  dismiss: "Dismiss",
15
15
  unpinApp: "Unpin this app",
16
16
  pinApp: "Pin this app",
17
+ reloadApp: "Reload app",
17
18
  removePinned: "Remove from pinned apps",
18
19
  pinTop: "Pin app to the top",
19
20
  moveUp: "Move up",
@@ -56,6 +56,7 @@ export interface ChatFirstAppRailProps {
56
56
  onLayoutError?: (reason: "unavailable" | "write-failed") => void;
57
57
  onRetry?: () => void;
58
58
  onOpenApp: (app: ChatFirstAppItem) => void;
59
+ onReloadApp?: (app: ChatFirstAppItem) => void;
59
60
  onRemoveApp?: (app: ChatFirstAppItem) => void;
60
61
  onOpenAllApps?: () => void;
61
62
  onCreateApp?: () => void;
@@ -28,6 +28,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- ok: boolean;
32
31
  error?: undefined;
32
+ ok: boolean;
33
33
  }>>;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- error: string;
52
51
  ok?: undefined;
52
+ error: string;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
37
37
  ok?: undefined;
38
38
  status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
44
  ok?: undefined;
45
45
  error: string;
46
46
  removed?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
58
58
  error: string;
59
59
  note?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.171.1",
3
+ "version": "0.171.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {