fix gc issue

This commit is contained in:
Arjun 2026-04-25 18:49:20 +05:30
parent 8cb55a903d
commit 8602330bbc
6 changed files with 24 additions and 55 deletions

View file

@ -24,30 +24,19 @@ export function extractDeepLinkFromArgv(argv: readonly string[]): string | null
}
export function dispatchDeepLink(url: string): void {
console.log(`[deeplink] dispatch ${url}`);
if (!url.startsWith(URL_PREFIX)) {
console.log(`[deeplink] rejected: bad prefix`);
return;
}
if (!url.startsWith(URL_PREFIX)) return;
pendingUrl = url;
const win = mainWindowRef;
if (!win || win.isDestroyed()) {
console.log(`[deeplink] no window, buffered`);
return;
}
if (!win || win.isDestroyed()) return;
if (win.isMinimized()) win.restore();
win.show();
win.focus();
if (win.webContents.isLoading()) {
console.log(`[deeplink] window loading, buffered`);
return;
}
if (win.webContents.isLoading()) return;
console.log(`[deeplink] sending app:openUrl to renderer`);
win.webContents.send("app:openUrl", { url });
pendingUrl = null;
}

View file

@ -6,38 +6,38 @@ const HTTP_URL = /^https?:\/\//i;
const ROWBOAT_URL = /^rowboat:\/\//i;
export class ElectronNotificationService implements INotificationService {
// Holds strong references to active Notification instances so the GC can't
// collect them while they're still visible — without this, the click handler
// gets dropped and macOS clicks just focus the app silently.
private active = new Set<Notification>();
isSupported(): boolean {
return Notification.isSupported();
}
notify({ title = "Rowboat", message, link, actionLabel }: NotifyInput): void {
notify({ title = "Rowboat", message, link }: NotifyInput): void {
const notification = new Notification({
title,
body: message,
// Action button is only meaningful when there's something to open.
// macOS shows the first action inline (Banner) or all (Alert).
actions: link ? [{ type: "button", text: actionLabel?.trim() || "Open" }] : [],
});
const handleAction = (source: string) => {
console.log(`[notification] ${source} fired, link=${link ?? '<none>'}`);
this.active.add(notification);
const release = () => { this.active.delete(notification); };
notification.on("click", () => {
if (link && ROWBOAT_URL.test(link)) {
dispatchDeepLink(link);
return;
}
if (link && HTTP_URL.test(link)) {
} else if (link && HTTP_URL.test(link)) {
shell.openExternal(link).catch((err) => {
console.error("[notification] failed to open link:", err);
});
return;
} else {
this.focusMainWindow();
}
this.focusMainWindow();
};
// Both events route through the same handler — body click on macOS is
// less reliable than action-button click, but we want either to work.
notification.on("click", () => handleAction("click"));
notification.on("action", () => handleAction("action"));
release();
});
notification.on("close", release);
notification.on("failed", release);
notification.show();
}

View file

@ -3097,14 +3097,11 @@ function App() {
useEffect(() => {
const handle = (url: string) => {
const view = parseDeepLink(url)
console.log('[deeplink renderer] received', url, '→ view', view)
if (view) void navigateToViewRef.current(view)
}
void window.ipc.invoke('app:consumePendingDeepLink', null).then(({ url }) => {
console.log('[deeplink renderer] mount drain:', url)
if (url) handle(url)
})
console.log('[deeplink renderer] listener registered')
return window.ipc.on('app:openUrl', ({ url }) => handle(url))
}, [])

View file

@ -14,15 +14,10 @@ Triggers a native macOS notification. The call returns immediately; it does not
### Parameters
- **\`title\`** (optional, defaults to \`"Rowboat"\`) — bold headline at the top.
- **\`message\`** (required) — body text. Keep it short — macOS truncates after a couple of lines.
- **\`link\`** (optional) — URL to open when the user clicks the notification or its action button. Two kinds accepted:
- **\`link\`** (optional) — URL to open when the user clicks the notification. Two kinds accepted:
- **\`https://...\` / \`http://...\`** — opens in the default browser
- **\`rowboat://...\`** — opens a view inside Rowboat (see deep links below)
- If omitted, clicking the notification focuses the Rowboat app.
- **\`actionLabel\`** (optional, defaults to \`"Open"\`) — label for the inline action button. Only shown when \`link\` is set. Keep it to 1-2 words: \`"Open"\`, \`"View"\`, \`"Read"\`, \`"Reply"\`. Pick a verb that names what clicking will do.
### Why the action button matters
When \`link\` is set, an action button is shown inline on the notification (the same way Slack shows "Reply" or Mail shows "Mark as Read"). This button is **the recommended click target** — it's a clear CTA and it's more reliable than expecting the user to click the notification body. Body click also works as a fallback.
### Examples
@ -43,7 +38,7 @@ External link:
}
\`\`\`
Deep link into a Rowboat note (default "Open" button):
Deep link into a Rowboat note:
\`\`\`json
{
"message": "Daily brief is ready",
@ -51,16 +46,6 @@ Deep link into a Rowboat note (default "Open" button):
}
\`\`\`
Custom action label:
\`\`\`json
{
"title": "Stripe charge declined",
"message": "Card ending 4242 — retry from the dashboard",
"link": "https://dashboard.stripe.com/payments/pi_abc",
"actionLabel": "Review"
}
\`\`\`
## Deep links: \`rowboat://\`
Use these as the \`link\` parameter to land the user on a specific view in Rowboat instead of an external site. URL-encode paths/names that contain spaces or special characters.

View file

@ -1524,7 +1524,6 @@ 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("Label for the action button shown when `link` is set. Defaults to 'Open'. Keep it short — 1-2 words like 'Open', 'View', 'Read', 'Reply'. Ignored when no link is provided."),
}),
isAvailable: async () => {
try {
@ -1533,13 +1532,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 }: { title?: string; message: 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 });
return { success: true };
} catch (error) {
return {

View file

@ -2,7 +2,6 @@ export interface NotifyInput {
title?: string;
message: string;
link?: string;
actionLabel?: string;
}
export interface INotificationService {