diff --git a/packages/cli/__tests__/lib/startup-notifier-defaults.test.ts b/packages/cli/__tests__/lib/startup-notifier-defaults.test.ts index 4aa51b959..240157ebe 100644 --- a/packages/cli/__tests__/lib/startup-notifier-defaults.test.ts +++ b/packages/cli/__tests__/lib/startup-notifier-defaults.test.ts @@ -159,6 +159,41 @@ describe("startup notifier defaults", () => { expect(parsed.notificationRouting?.urgent).toEqual(["desktop", "dashboard"]); }); + it("preserves defaults.notifiers fallback semantics when writing startup routes", () => { + writeFileSync( + configPath, + [ + "port: 3000", + "defaults:", + " notifiers:", + " - slack", + "notifiers:", + " slack:", + " plugin: slack", + " webhookUrl: https://hooks.slack.com/services/T/B/C", + "projects: {}", + "", + ].join("\n"), + ); + + expect( + ensureStartupNotifierDefaults({ + configPath, + dashboardUrl: "http://localhost:3000", + desktopMode: "enable", + }), + ).toBe(true); + + const parsed = readConfig(); + expect(parsed.defaults?.notifiers).toEqual(["slack"]); + expect(parsed.notificationRouting).toEqual({ + urgent: ["slack", "desktop", "dashboard"], + action: ["slack", "dashboard"], + warning: ["slack", "dashboard"], + info: ["slack", "dashboard"], + }); + }); + it("preserves configured manual opt-in notifiers while removing only implicit manual defaults", () => { writeFileSync( configPath, diff --git a/packages/cli/src/lib/startup-notifier-defaults.ts b/packages/cli/src/lib/startup-notifier-defaults.ts index cdc299f27..291f0b6d9 100644 --- a/packages/cli/src/lib/startup-notifier-defaults.ts +++ b/packages/cli/src/lib/startup-notifier-defaults.ts @@ -44,6 +44,10 @@ function arraysEqual(left: string[], right: string[]): boolean { return left.length === right.length && left.every((value, index) => value === right[index]); } +function hasOwn(record: Record, key: string): boolean { + return Object.prototype.hasOwnProperty.call(record, key); +} + function hasNotifierConfig(notifiers: Record, notifierName: string): boolean { return isRecord(notifiers[notifierName]); } @@ -227,7 +231,12 @@ export function ensureStartupNotifierDefaults(options: StartupNotifierDefaultsOp const shouldRemoveDefaultDesktopRouting = desktopMode === "disable-default" && !desktopConfigured; for (const priority of NOTIFICATION_PRIORITIES) { - const existingRoute = sanitizeNotifierReferences(notifiers, routing[priority]); + const hasExplicitRoute = hasOwn(routing, priority); + const existingRoute = sanitizeNotifierReferences( + notifiers, + hasExplicitRoute ? routing[priority] : defaults["notifiers"], + ); + const previousRoute = hasExplicitRoute ? asStringArray(routing[priority]) : existingRoute; let nextRoute = existingRoute; if (shouldRemoveDefaultDesktopRouting) { @@ -245,7 +254,7 @@ export function ensureStartupNotifierDefaults(options: StartupNotifierDefaultsOp } nextRoute = unique(nextRoute); - if (!arraysEqual(asStringArray(routing[priority]), nextRoute)) { + if (!arraysEqual(previousRoute, nextRoute)) { routing[priority] = nextRoute; changed = true; } diff --git a/packages/plugins/notifier-desktop/src/index.test.ts b/packages/plugins/notifier-desktop/src/index.test.ts index 1f7755c41..21cba3776 100644 --- a/packages/plugins/notifier-desktop/src/index.test.ts +++ b/packages/plugins/notifier-desktop/src/index.test.ts @@ -483,6 +483,18 @@ describe("notifier-desktop", () => { expect(mockExecFile.mock.calls[0][0]).toBe("notify-send"); }); + it("keeps action labels on non-macOS even when backend is ao-app", async () => { + mockPlatform.mockReturnValue("linux"); + setProcessPlatform("linux"); + const notifier = create({ backend: "ao-app", dashboardUrl: "http://localhost:3000" }); + const actions: NotifyAction[] = [{ label: "Open PR", url: "https://example.com/pr/1" }]; + await notifier.notifyWithActions!(makeEvent(), actions); + + expect(mockExecFile.mock.calls[0][0]).toBe("notify-send"); + const args = mockExecFile.mock.calls[0][1] as string[]; + expect(args.join("\n")).toContain("Open PR"); + }); + it("uses terminal-notifier for notifyWithActions too", async () => { const notifier = create({ dashboardUrl: "http://localhost:3000" }); const actions: NotifyAction[] = [{ label: "View", url: "https://example.com" }]; diff --git a/packages/plugins/notifier-desktop/src/index.ts b/packages/plugins/notifier-desktop/src/index.ts index 47bee1d25..afce79928 100644 --- a/packages/plugins/notifier-desktop/src/index.ts +++ b/packages/plugins/notifier-desktop/src/index.ts @@ -388,6 +388,10 @@ function detectTerminalNotifier(): boolean { } } +function usesAoNotifierNativeActions(backend: DesktopBackend, appPath: string): boolean { + return isMac() && (backend === "ao-app" || (backend === "auto" && detectAoNotifierApp(appPath))); +} + /** * Send a desktop notification using terminal-notifier / osascript (macOS) or * notify-send (Linux). Falls back gracefully if neither is available. @@ -568,10 +572,9 @@ export function create(config?: Record): Notifier { async notifyWithActions(event: OrchestratorEvent, actions: NotifyAction[]): Promise { const nativeActions = nativeActionPayloads(actions, dashboardUrl); const content = formatContent(event, actions, { - hiddenActionIndexes: - backend === "ao-app" || (backend === "auto" && detectAoNotifierApp(appPath)) - ? nativeActionIndexes(actions, dashboardUrl) - : undefined, + hiddenActionIndexes: usesAoNotifierNativeActions(backend, appPath) + ? nativeActionIndexes(actions, dashboardUrl) + : undefined, }); const fallbackContent = formatContent(event, actions); const sound = shouldPlaySound(event.priority, soundEnabled);