From 37da70513e9fbb15544c9d17ec3d56e9691469f1 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 9 Feb 2026 08:43:25 -0800 Subject: [PATCH 1/3] Navigate on notif click --- .../_dashboard/utils/workspace-navigation.ts | 13 +++++++-- .../workspace/$workspaceId/page.tsx | 28 ++++++++++++++++++- .../stores/tabs/useAgentHookListener.ts | 27 ++++++------------ 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts index 9a43959ac4b..cc5d7f3463c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts @@ -3,6 +3,11 @@ import type { UseNavigateResult, } from "@tanstack/react-router"; +export interface WorkspaceSearchParams { + tabId?: string; + paneId?: string; +} + /** * Navigate to a workspace and update localStorage to remember it as the last viewed workspace. * This ensures the workspace will be restored when the app is reopened. @@ -14,12 +19,16 @@ import type { export function navigateToWorkspace( workspaceId: string, navigate: UseNavigateResult, - options?: Omit, + options?: Omit & { + search?: WorkspaceSearchParams; + }, ): Promise { + const { search, ...rest } = options ?? {}; localStorage.setItem("lastViewedWorkspaceId", workspaceId); return navigate({ to: "/workspace/$workspaceId", params: { workspaceId }, - ...options, + search: search ?? {}, + ...rest, }); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx index ac5731d3285..87cb1da6cf5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx @@ -1,9 +1,10 @@ import { toast } from "@superset/ui/sonner"; import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router"; -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { electronTrpcClient as trpcClient } from "renderer/lib/trpc-client"; import { usePresets } from "renderer/react-query/presets"; +import type { WorkspaceSearchParams } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { usePresetHotkeys } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys"; import { NotFound } from "renderer/routes/not-found"; @@ -33,6 +34,10 @@ export const Route = createFileRoute( )({ component: WorkspacePage, notFoundComponent: NotFound, + validateSearch: (search: Record): WorkspaceSearchParams => ({ + tabId: typeof search.tabId === "string" ? search.tabId : undefined, + paneId: typeof search.paneId === "string" ? search.paneId : undefined, + }), loader: async ({ params, context }) => { const queryKey = [ ["workspaces", "get"], @@ -62,6 +67,27 @@ function WorkspacePage() { id: workspaceId, }); const navigate = useNavigate(); + const routeNavigate = Route.useNavigate(); + const { tabId: searchTabId, paneId: searchPaneId } = Route.useSearch(); + + // Handle search-param-driven tab/pane activation (e.g. from notification clicks) + useEffect(() => { + if (!searchTabId) return; + + const state = useTabsStore.getState(); + const tab = state.tabs.find( + (t) => t.id === searchTabId && t.workspaceId === workspaceId, + ); + if (!tab) return; + + state.setActiveTab(workspaceId, searchTabId); + + if (searchPaneId && state.panes[searchPaneId]) { + state.setFocusedPane(searchTabId, searchPaneId); + } + + routeNavigate({ search: {}, replace: true }); + }, [searchTabId, searchPaneId, workspaceId, routeNavigate]); // Check if workspace is initializing or failed const isInitializing = useIsWorkspaceInitializing(workspaceId); diff --git a/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts b/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts index af47ef775ab..026c250d8ed 100644 --- a/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts +++ b/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts @@ -94,24 +94,15 @@ export function useAgentHookListener() { state.setPaneStatus(paneId, "idle"); } } else if (event.type === NOTIFICATION_EVENTS.FOCUS_TAB) { - navigateToWorkspace(workspaceId, navigate); - - // Re-fetch state after navigation since router nav is async but state updates are immediate - const freshState = useTabsStore.getState(); - const freshTarget = resolveNotificationTarget(event.data, freshState); - - const tabIdToActivate = freshTarget?.tabId ?? event.data?.tabId; - if (!tabIdToActivate) return; - - const freshTab = freshState.tabs.find((t) => t.id === tabIdToActivate); - if (!freshTab || freshTab.workspaceId !== workspaceId) return; - - freshState.setActiveTab(workspaceId, tabIdToActivate); - - const paneIdToFocus = freshTarget?.paneId ?? event.data?.paneId; - if (paneIdToFocus && freshState.panes[paneIdToFocus]) { - freshState.setFocusedPane(tabIdToActivate, paneIdToFocus); - } + const tabIdToActivate = target.tabId ?? event.data?.tabId; + const paneIdToFocus = target.paneId ?? event.data?.paneId; + + navigateToWorkspace(workspaceId, navigate, { + search: { + tabId: tabIdToActivate, + paneId: paneIdToFocus, + }, + }); } }, }); From 39f14cc0101d54a532766f299953e275606615f5 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 9 Feb 2026 13:25:21 -0800 Subject: [PATCH 2/3] Clean up --- .../src/renderer/stores/tabs/useAgentHookListener.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts b/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts index 026c250d8ed..fcde2bf4ccd 100644 --- a/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts +++ b/apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts @@ -94,13 +94,10 @@ export function useAgentHookListener() { state.setPaneStatus(paneId, "idle"); } } else if (event.type === NOTIFICATION_EVENTS.FOCUS_TAB) { - const tabIdToActivate = target.tabId ?? event.data?.tabId; - const paneIdToFocus = target.paneId ?? event.data?.paneId; - navigateToWorkspace(workspaceId, navigate, { search: { - tabId: tabIdToActivate, - paneId: paneIdToFocus, + tabId: target.tabId, + paneId: target.paneId, }, }); } From 6cdde6df5f7d6ebedda079206909c1ca944ed927 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 9 Feb 2026 14:08:01 -0800 Subject: [PATCH 3/3] Extract NotificationManager to fix macOS notification click handlers Electron Notification objects get garbage collected on macOS while the native notification is still visible, silently dropping click handlers. Extract notification lifecycle into a testable NotificationManager that retains references, replaces per-pane, and sweeps stale entries on a TTL. --- .../notification-manager.test.ts | 264 ++++++++++++++++++ .../lib/notifications/notification-manager.ts | 148 ++++++++++ apps/desktop/src/main/windows/main.ts | 95 ++----- 3 files changed, 442 insertions(+), 65 deletions(-) create mode 100644 apps/desktop/src/main/lib/notifications/notification-manager.test.ts create mode 100644 apps/desktop/src/main/lib/notifications/notification-manager.ts diff --git a/apps/desktop/src/main/lib/notifications/notification-manager.test.ts b/apps/desktop/src/main/lib/notifications/notification-manager.test.ts new file mode 100644 index 00000000000..aaa7f62aaa2 --- /dev/null +++ b/apps/desktop/src/main/lib/notifications/notification-manager.test.ts @@ -0,0 +1,264 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test"; +import type { + AgentLifecycleEvent, + NotificationIds, +} from "shared/notification-types"; +import { + type NativeNotification, + NotificationManager, + type NotificationManagerDeps, +} from "./notification-manager"; + +type MockNotification = NativeNotification & { + handlers: Record void)[]>; + trigger: (event: string) => void; +}; + +function createMockNotification(): MockNotification { + const handlers: Record void)[]> = {}; + return { + handlers, + show: mock(() => {}), + close: mock(() => {}), + on: mock((event: string, handler: () => void) => { + handlers[event] ??= []; + handlers[event].push(handler); + }), + trigger(event: string) { + for (const handler of handlers[event] ?? []) handler(); + }, + }; +} + +interface TestDeps extends NotificationManagerDeps { + notifications: MockNotification[]; + clickedIds: NotificationIds[]; +} + +function createDeps( + overrides: Partial = {}, +): TestDeps { + const notifications: MockNotification[] = []; + const clickedIds: NotificationIds[] = []; + + return { + notifications, + clickedIds, + isSupported: () => true, + createNotification: () => { + const n = createMockNotification(); + notifications.push(n); + return n; + }, + playSound: mock(() => {}), + onNotificationClick: (ids) => clickedIds.push(ids), + getVisibilityContext: () => ({ + isFocused: false, + currentWorkspaceId: null, + tabsState: undefined, + }), + getWorkspaceName: () => "Test Workspace", + getNotificationTitle: () => "Test Title", + ...overrides, + }; +} + +function lastNotification(deps: TestDeps): MockNotification { + return deps.notifications[deps.notifications.length - 1]; +} + +function makeEvent( + overrides: Partial = {}, +): AgentLifecycleEvent { + return { + eventType: "Stop", + paneId: "pane-1", + tabId: "tab-1", + workspaceId: "ws-1", + ...overrides, + }; +} + +describe("NotificationManager", () => { + let deps: TestDeps; + let manager: NotificationManager; + + beforeEach(() => { + deps = createDeps(); + manager = new NotificationManager(deps); + }); + + describe("handleAgentLifecycle", () => { + it("ignores Start events", () => { + manager.handleAgentLifecycle(makeEvent({ eventType: "Start" })); + expect(manager.activeCount).toBe(0); + }); + + it("shows notification for Stop events", () => { + manager.handleAgentLifecycle(makeEvent({ eventType: "Stop" })); + expect(manager.activeCount).toBe(1); + expect(lastNotification(deps).show).toHaveBeenCalled(); + }); + + it("shows notification for PermissionRequest events", () => { + manager.handleAgentLifecycle( + makeEvent({ eventType: "PermissionRequest" }), + ); + expect(manager.activeCount).toBe(1); + }); + + it("does not show when isSupported returns false", () => { + const localDeps = createDeps({ isSupported: () => false }); + const localManager = new NotificationManager(localDeps); + localManager.handleAgentLifecycle(makeEvent()); + expect(localManager.activeCount).toBe(0); + }); + + it("plays sound on notification", () => { + manager.handleAgentLifecycle(makeEvent()); + expect(deps.playSound).toHaveBeenCalled(); + }); + }); + + describe("tracking and replacement", () => { + it("replaces notification for the same paneId", () => { + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-1" })); + const first = lastNotification(deps); + expect(manager.activeCount).toBe(1); + + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-1" })); + expect(manager.activeCount).toBe(1); + expect(first.close).toHaveBeenCalled(); + }); + + it("tracks different panes independently", () => { + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-1" })); + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-2" })); + expect(manager.activeCount).toBe(2); + }); + + it("untracks on click", () => { + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-1" })); + lastNotification(deps).trigger("click"); + expect(manager.activeCount).toBe(0); + }); + + it("untracks on close", () => { + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-1" })); + lastNotification(deps).trigger("close"); + expect(manager.activeCount).toBe(0); + }); + + it("fires onNotificationClick with correct ids on click", () => { + const event = makeEvent({ + paneId: "p1", + tabId: "t1", + workspaceId: "w1", + }); + manager.handleAgentLifecycle(event); + lastNotification(deps).trigger("click"); + expect(deps.clickedIds).toEqual([ + { paneId: "p1", tabId: "t1", workspaceId: "w1" }, + ]); + }); + + it("assigns unique keys when paneId is missing", () => { + manager.handleAgentLifecycle(makeEvent({ paneId: undefined })); + manager.handleAgentLifecycle(makeEvent({ paneId: undefined })); + expect(manager.activeCount).toBe(2); + }); + }); + + describe("visibility suppression", () => { + it("suppresses notification when pane is visible and window focused", () => { + const localDeps = createDeps({ + getVisibilityContext: () => ({ + isFocused: true, + currentWorkspaceId: "ws-1", + tabsState: { + activeTabIds: { "ws-1": "tab-1" }, + focusedPaneIds: { "tab-1": "pane-1" }, + }, + }), + }); + const localManager = new NotificationManager(localDeps); + + localManager.handleAgentLifecycle( + makeEvent({ + paneId: "pane-1", + tabId: "tab-1", + workspaceId: "ws-1", + }), + ); + expect(localManager.activeCount).toBe(0); + }); + + it("does not suppress when window is not focused", () => { + const localDeps = createDeps({ + getVisibilityContext: () => ({ + isFocused: false, + currentWorkspaceId: "ws-1", + tabsState: { + activeTabIds: { "ws-1": "tab-1" }, + focusedPaneIds: { "tab-1": "pane-1" }, + }, + }), + }); + const localManager = new NotificationManager(localDeps); + + localManager.handleAgentLifecycle(makeEvent()); + expect(localManager.activeCount).toBe(1); + }); + }); + + describe("dispose", () => { + it("clears all tracked notifications", () => { + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-1" })); + manager.handleAgentLifecycle(makeEvent({ paneId: "pane-2" })); + expect(manager.activeCount).toBe(2); + + manager.dispose(); + expect(manager.activeCount).toBe(0); + }); + }); + + describe("notification content", () => { + it("uses permission request title/body for PermissionRequest events", () => { + const createNotification = mock( + (_opts: { title: string; body: string; silent: boolean }) => + createMockNotification(), + ); + const localDeps = createDeps({ createNotification }); + const localManager = new NotificationManager(localDeps); + + localManager.handleAgentLifecycle( + makeEvent({ eventType: "PermissionRequest" }), + ); + + expect(createNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Input Needed — Test Workspace", + body: '"Test Title" needs your attention', + }), + ); + }); + + it("uses completion title/body for Stop events", () => { + const createNotification = mock( + (_opts: { title: string; body: string; silent: boolean }) => + createMockNotification(), + ); + const localDeps = createDeps({ createNotification }); + const localManager = new NotificationManager(localDeps); + + localManager.handleAgentLifecycle(makeEvent({ eventType: "Stop" })); + + expect(createNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Agent Complete — Test Workspace", + body: '"Test Title" has finished its task', + }), + ); + }); + }); +}); diff --git a/apps/desktop/src/main/lib/notifications/notification-manager.ts b/apps/desktop/src/main/lib/notifications/notification-manager.ts new file mode 100644 index 00000000000..9db0ceed283 --- /dev/null +++ b/apps/desktop/src/main/lib/notifications/notification-manager.ts @@ -0,0 +1,148 @@ +import type { + AgentLifecycleEvent, + NotificationIds, +} from "shared/notification-types"; +import { isPaneVisible } from "./utils"; + +const NOTIFICATION_TTL_MS = 10 * 60 * 1000; +const SWEEP_INTERVAL_MS = 5 * 60 * 1000; + +export interface NativeNotification { + show(): void; + close(): void; + on(event: "click", handler: () => void): void; + on(event: "close", handler: () => void): void; +} + +export interface NotificationManagerDeps { + isSupported: () => boolean; + createNotification: (opts: { + title: string; + body: string; + silent: boolean; + }) => NativeNotification; + playSound: () => void; + onNotificationClick: (ids: NotificationIds) => void; + getVisibilityContext: () => { + isFocused: boolean; + currentWorkspaceId: string | null; + tabsState: + | { + activeTabIds?: Record; + focusedPaneIds?: Record; + } + | undefined; + }; + getWorkspaceName: (workspaceId: string | undefined) => string; + getNotificationTitle: (event: AgentLifecycleEvent) => string; +} + +interface TrackedEntry { + notification: NativeNotification; + createdAt: number; +} + +export class NotificationManager { + private active = new Map(); + private counter = 0; + private sweepTimer: ReturnType | null = null; + + constructor(private deps: NotificationManagerDeps) {} + + start(): void { + if (this.sweepTimer) return; + this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS); + } + + handleAgentLifecycle(event: AgentLifecycleEvent): void { + if (event.eventType === "Start") return; + if (!this.deps.isSupported()) return; + + if (this.shouldSuppressForVisiblePane(event)) return; + + const workspaceName = this.deps.getWorkspaceName(event.workspaceId); + const title = this.deps.getNotificationTitle(event); + + const isPermissionRequest = event.eventType === "PermissionRequest"; + const notification = this.deps.createNotification({ + title: isPermissionRequest + ? `Input Needed — ${workspaceName}` + : `Agent Complete — ${workspaceName}`, + body: isPermissionRequest + ? `"${title}" needs your attention` + : `"${title}" has finished its task`, + silent: true, + }); + + const key = event.paneId ?? `_anon_${this.counter++}`; + this.track(key, notification); + + this.deps.playSound(); + + notification.on("click", () => { + this.deps.onNotificationClick({ + paneId: event.paneId, + tabId: event.tabId, + workspaceId: event.workspaceId, + }); + this.untrack(key); + }); + + notification.on("close", () => { + this.untrack(key); + }); + + notification.show(); + } + + /** Number of tracked notifications (for testing). */ + get activeCount(): number { + return this.active.size; + } + + dispose(): void { + if (this.sweepTimer) { + clearInterval(this.sweepTimer); + this.sweepTimer = null; + } + this.active.clear(); + } + + private shouldSuppressForVisiblePane(event: AgentLifecycleEvent): boolean { + if (!event.workspaceId || !event.tabId || !event.paneId) return false; + + const ctx = this.deps.getVisibilityContext(); + if (!ctx.isFocused) return false; + + return isPaneVisible({ + currentWorkspaceId: ctx.currentWorkspaceId, + tabsState: ctx.tabsState, + pane: { + workspaceId: event.workspaceId, + tabId: event.tabId, + paneId: event.paneId, + }, + }); + } + + private track(key: string, notification: NativeNotification): void { + const prev = this.active.get(key); + if (prev) { + prev.notification.close(); + } + this.active.set(key, { notification, createdAt: Date.now() }); + } + + private untrack(key: string): void { + this.active.delete(key); + } + + private sweep(): void { + const now = Date.now(); + for (const [key, entry] of this.active) { + if (now - entry.createdAt > NOTIFICATION_TTL_MS) { + this.active.delete(key); + } + } + } +} diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index b993b1edd71..9a692d2056a 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -7,13 +7,14 @@ import { createWindow } from "lib/electron-app/factories/windows/create"; import { createAppRouter } from "lib/trpc/routers"; import { localDb } from "main/lib/local-db"; import { NOTIFICATION_EVENTS, PLATFORM, PORTS } from "shared/constants"; +import type { AgentLifecycleEvent } from "shared/notification-types"; import { createIPCHandler } from "trpc-electron/main"; import { productName } from "~/package.json"; import { appState } from "../lib/app-state"; import { createApplicationMenu, registerMenuHotkeyUpdates } from "../lib/menu"; import { playNotificationSound } from "../lib/notification-sound"; +import { NotificationManager } from "../lib/notifications/notification-manager"; import { - type AgentLifecycleEvent, notificationsApp, notificationsEmitter, } from "../lib/notifications/server"; @@ -21,7 +22,6 @@ import { extractWorkspaceIdFromUrl, getNotificationTitle, getWorkspaceName, - isPaneVisible, } from "../lib/notifications/utils"; import { getInitialWindowBounds, @@ -55,10 +55,9 @@ function getWorkspaceNameFromDb(workspaceId: string | undefined): string { } } -// Current window reference - updated on window create/close let currentWindow: BrowserWindow | null = null; -// Getter for routers to access current window without stale references +// Routers receive this getter so they always see the current window, not a stale reference const getWindow = () => currentWindow; // invalidate() alone may not rebuild corrupted GPU layers — a tiny resize @@ -134,7 +133,6 @@ export async function MainWindow() { }); } - // Start notifications HTTP server const server = notificationsApp.listen( PORTS.NOTIFICATIONS, "127.0.0.1", @@ -145,68 +143,37 @@ export async function MainWindow() { }, ); - // Handle agent lifecycle notifications (Stop = completion, PermissionRequest = needs input) - notificationsEmitter.on( - NOTIFICATION_EVENTS.AGENT_LIFECYCLE, - (event: AgentLifecycleEvent) => { - // Only notify on Stop (completion) and PermissionRequest - not on Start - if (event.eventType === "Start") return; - - // Skip notification if user is already viewing this pane (Slack pattern) - if ( - window.isFocused() && - event.workspaceId && - event.tabId && - event.paneId - ) { - const isVisible = isPaneVisible({ - currentWorkspaceId: extractWorkspaceIdFromUrl( - window.webContents.getURL(), - ), - tabsState: appState.data?.tabsState, - pane: { - workspaceId: event.workspaceId, - tabId: event.tabId, - paneId: event.paneId, - }, - }); - if (isVisible) return; - } - - if (!Notification.isSupported()) return; - - const workspaceName = getWorkspaceNameFromDb(event.workspaceId); - const title = getNotificationTitle({ + const notificationManager = new NotificationManager({ + isSupported: () => Notification.isSupported(), + createNotification: (opts) => new Notification(opts), + playSound: playNotificationSound, + onNotificationClick: (ids) => { + window.show(); + window.focus(); + notificationsEmitter.emit(NOTIFICATION_EVENTS.FOCUS_TAB, ids); + }, + getVisibilityContext: () => ({ + isFocused: window.isFocused(), + currentWorkspaceId: extractWorkspaceIdFromUrl( + window.webContents.getURL(), + ), + tabsState: appState.data?.tabsState, + }), + getWorkspaceName: getWorkspaceNameFromDb, + getNotificationTitle: (event) => + getNotificationTitle({ tabId: event.tabId, paneId: event.paneId, tabs: appState.data?.tabsState?.tabs, panes: appState.data?.tabsState?.panes, - }); - - const isPermissionRequest = event.eventType === "PermissionRequest"; - const notification = new Notification({ - title: isPermissionRequest - ? `Input Needed — ${workspaceName}` - : `Agent Complete — ${workspaceName}`, - body: isPermissionRequest - ? `"${title}" needs your attention` - : `"${title}" has finished its task`, - silent: true, - }); - - playNotificationSound(); - - notification.on("click", () => { - window.show(); - window.focus(); - notificationsEmitter.emit(NOTIFICATION_EVENTS.FOCUS_TAB, { - paneId: event.paneId, - tabId: event.tabId, - workspaceId: event.workspaceId, - }); - }); + }), + }); + notificationManager.start(); - notification.show(); + notificationsEmitter.on( + NOTIFICATION_EVENTS.AGENT_LIFECYCLE, + (event: AgentLifecycleEvent) => { + notificationManager.handleAgentLifecycle(event); }, ); @@ -244,11 +211,9 @@ export async function MainWindow() { window.webContents.on("did-finish-load", async () => { console.log("[main-window] Renderer loaded successfully"); - // Restore maximized state if it was saved if (initialBounds.isMaximized) { window.maximize(); } - // Restore zoom level if it was saved if (savedWindowState?.zoomLevel !== undefined) { window.webContents.setZoomLevel(savedWindowState.zoomLevel); } @@ -292,12 +257,12 @@ export async function MainWindow() { }); server.close(); + notificationManager.dispose(); notificationsEmitter.removeAllListeners(); // Remove terminal listeners to prevent duplicates when window reopens on macOS getWorkspaceRuntimeRegistry().getDefault().terminal.detachAllListeners(); // Detach window from IPC handler (handler stays alive for window reopen) ipcHandler?.detachWindow(window); - // Clear current window reference currentWindow = null; });