option to join meeting and take notes

This commit is contained in:
Arjun 2026-04-25 21:44:40 +05:30
parent d363363b70
commit 0107dc5dbf
7 changed files with 84 additions and 36 deletions

View file

@ -1524,7 +1524,13 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
link: z.string().url().refine((v) => /^(https?|rowboat):\/\//i.test(v), {
message: "link must be an http(s):// or rowboat:// URL",
}).optional().describe("Optional URL opened when the user clicks the notification. Accepts http(s):// (opens in browser) or rowboat:// (opens a view inside Rowboat — see the notify-user skill for deep-link shapes)."),
actionLabel: z.string().min(1).max(20).optional().describe("Optional label for an inline action button on the notification (e.g. 'Open', 'View', 'Take Notes'). Only shown when `link` is set. Click on the button triggers the same action as clicking the notification body. Note: macOS may render the button inline (Banner) or behind a chevron (Alert) depending on the user's notification style for Rowboat."),
actionLabel: z.string().min(1).max(20).optional().describe("Optional label for an inline action button on the notification (e.g. 'Open', 'View', 'Take Notes'). Only shown when `link` is set. Click on the button triggers the same action as clicking the notification body."),
secondaryActions: z.array(z.object({
label: z.string().min(1).max(30),
link: z.string().url().refine((v) => /^(https?|rowboat):\/\//i.test(v), {
message: "secondary action link must be an http(s):// or rowboat:// URL",
}),
})).max(4).optional().describe("Additional action buttons. macOS shows them in the chevron menu next to the primary button (or all inline in Alert style). Each has its own label and link — clicking the button triggers that link, independent of the primary `link`."),
}),
isAvailable: async () => {
try {
@ -1533,13 +1539,13 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
return false;
}
},
execute: async ({ title, message, link, actionLabel }: { title?: string; message: string; link?: string; actionLabel?: string }) => {
execute: async ({ title, message, link, actionLabel, secondaryActions }: { title?: string; message: string; link?: string; actionLabel?: string; secondaryActions?: Array<{ label: string; link: string }> }) => {
try {
const service = container.resolve<INotificationService>('notificationService');
if (!service.isSupported()) {
return { success: false, error: 'Notifications are not supported on this system' };
}
service.notify({ title, message, link, actionLabel });
service.notify({ title, message, link, actionLabel, secondaryActions });
return { success: true };
} catch (error) {
return {

View file

@ -3,6 +3,7 @@ export interface NotifyInput {
message: string;
link?: string;
actionLabel?: string;
secondaryActions?: Array<{ label: string; link: string }>;
}
export interface INotificationService {

View file

@ -121,14 +121,22 @@ async function tick(state: NotificationState): Promise<{ state: NotificationStat
if (msUntilStart < -NOTIFY_GRACE_MS) continue;
const summary = event.summary?.trim() || "Untitled meeting";
const link = `rowboat://action?type=take-meeting-notes&eventId=${encodeURIComponent(eventId)}`;
const eid = encodeURIComponent(eventId);
try {
service.notify({
title: "Upcoming meeting",
message: `${summary} starts in 1 minute. Click to take notes.`,
link,
actionLabel: "Take Notes",
message: `${summary} starts in 1 minute. Click to join and take notes.`,
// Primary (body click + first button): join the meeting AND take notes.
link: `rowboat://action?type=join-and-take-meeting-notes&eventId=${eid}`,
actionLabel: "Join meeting and take notes",
// Behind the chevron: just take notes (no join).
secondaryActions: [
{
label: "Take notes",
link: `rowboat://action?type=take-meeting-notes&eventId=${eid}`,
},
],
});
console.log(`[CalendarNotify] notified for "${summary}" (${eventId})`);
} catch (err) {

View file

@ -302,6 +302,9 @@ const ipcSchemas = {
req: z.object({
// Pass the raw calendar event JSON through; renderer adapts to its existing flow.
event: z.unknown(),
// When true, the renderer should also open the meeting URL (Zoom/Meet/etc.)
// in addition to triggering the take-notes flow.
openMeeting: z.boolean().optional(),
}),
res: z.null(),
},