@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
- package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
- package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
- package/corpus/templates/analytics/AGENTS.md +3 -0
- package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
- package/corpus/templates/analytics/actions/bigquery.ts +1 -1
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
- package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
- package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
- package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/calendar/AGENTS.md +2 -0
- package/corpus/templates/calendar/DEVELOPING.md +4 -4
- package/corpus/templates/calendar/actions/get-settings.ts +4 -11
- package/corpus/templates/calendar/actions/update-availability.ts +15 -4
- package/corpus/templates/calendar/actions/update-settings.ts +15 -4
- package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
- package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
- package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
- package/corpus/templates/calendar/app/i18n-data.ts +30 -0
- package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
- package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
- package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
- package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
- package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
- package/corpus/templates/calendar/shared/api.ts +1 -0
- package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
- package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
- package/corpus/templates/calendar/shared/settings.ts +44 -0
- package/corpus/templates/clips/AGENTS.md +2 -0
- package/corpus/templates/clips/actions/add-comment.ts +5 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
- package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
- package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
- package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
- package/corpus/templates/clips/actions/update-comment.ts +9 -2
- package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
- package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
- package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
- package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
- package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
- package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
- package/corpus/templates/clips/app/root.tsx +2 -1
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -7
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +8 -4
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
- package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +116 -19
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
- package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
- package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
- package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
- package/corpus/templates/clips/desktop/src/styles.css +117 -0
- package/corpus/templates/clips/server/db/index.ts +10 -0
- package/corpus/templates/clips/server/db/schema.ts +1 -0
- package/corpus/templates/clips/server/lib/recordings.ts +3 -1
- package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
- package/corpus/templates/clips/server/plugins/db.ts +5 -0
- package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
- package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
- package/corpus/templates/content/actions/add-comment.ts +2 -1
- package/corpus/templates/content/actions/update-comment.ts +1 -1
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
- package/corpus/templates/design/actions/generate-design.ts +10 -7
- package/corpus/templates/design/actions/present-design-variants.ts +35 -6
- package/corpus/templates/design/actions/run-design-audit.ts +33 -0
- package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
- package/corpus/templates/design/shared/design-review.ts +1 -0
- package/corpus/templates/design/shared/html-integrity.ts +66 -0
- package/corpus/templates/dispatch/.env.example +5 -0
- package/corpus/templates/dispatch/AGENTS.md +24 -19
- package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
- package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
- package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
- package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
- package/corpus/templates/factory/AGENTS.md +5 -1
- package/corpus/templates/factory/README.md +23 -0
- package/corpus/templates/factory/actions/view-screen.ts +17 -0
- package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
- package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
- package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
- package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
- package/corpus/templates/factory/app/routes/factory.tsx +41 -0
- package/corpus/templates/factory/package.json +1 -0
- package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
- package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
- package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
- package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
- package/corpus/templates/mail/vite.config.ts +7 -0
- package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
- package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
- package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
- package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
- package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
- package/dist/agent/production-agent.js +5 -1
- package/dist/agent/thread-data-builder.js +17 -1
- package/dist/cli/skills-content/help.js +6 -4
- package/dist/cli/skills.js +17 -2
- package/dist/cli/workspacify.d.ts +5 -0
- package/dist/cli/workspacify.js +65 -8
- package/dist/client/AgentPanel.d.ts +5 -0
- package/dist/client/AgentPanel.js +20 -4
- package/dist/client/AssistantChat.d.ts +21 -0
- package/dist/client/AssistantChat.js +66 -68
- package/dist/client/active-run-state.d.ts +14 -1
- package/dist/client/active-run-state.js +0 -0
- package/dist/client/agent-chat-adapter.js +107 -5
- package/dist/client/analytics.js +9 -2
- package/dist/client/chat/tool-call-display.js +4 -4
- package/dist/client/chat-first/apps-rail.js +9 -4
- package/dist/client/chat-first/primary-nav.d.ts +2 -1
- package/dist/client/chat-first/primary-nav.js +3 -3
- package/dist/client/chat-first/types.d.ts +5 -1
- package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
- package/dist/client/markdown/InlineMarkdown.js +65 -0
- package/dist/client/markdown/index.d.ts +1 -0
- package/dist/client/markdown/index.js +1 -0
- package/dist/client/review/ReviewThreadPanel.js +2 -1
- package/dist/client/run-stream-ownership.d.ts +39 -0
- package/dist/client/run-stream-ownership.js +65 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/feature-flags/index.d.ts +1 -1
- package/dist/feature-flags/index.js +1 -1
- package/dist/feature-flags/store.d.ts +10 -0
- package/dist/feature-flags/store.js +112 -1
- package/dist/guards/no-env-credentials.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/integrations/adapters/slack.js +5 -1
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +1 -0
- package/dist/observability/routes.d.ts +6 -6
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +15 -15
- package/dist/resources/agents.js +40 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/metadata.d.ts +10 -0
- package/dist/resources/metadata.js +7 -3
- package/dist/review/actions/create-review-comment.js +1 -1
- package/dist/review/actions/reply-review-comment.js +1 -1
- package/dist/review/actions/resolve-review-thread.js +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/action-routes.js +32 -6
- package/dist/server/agent-discovery.js +14 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.js +1 -1
- package/dist/server/identity-sso-migrations.d.ts +7 -0
- package/dist/server/identity-sso-migrations.js +36 -0
- package/dist/server/identity-sso-store.d.ts +3 -2
- package/dist/server/identity-sso-store.js +8 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/release-migrations.js +4 -0
- package/dist/tracking/providers.js +13 -3
- package/dist/workspace-files/tool.js +1 -1
- 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:
|
|
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")}
|
|
@@ -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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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);
|