diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 58f8748582..c93fb7aaca 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -44,7 +44,7 @@ import { } from "@/features/notifications/hooks"; import { PreventSleepProvider } from "@/features/agents/usePreventSleep"; import { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent"; -import { isAgentActivityWindow } from "@/features/agents/lib/agentActivityWindow"; +import { currentCompanionWindowKind } from "@/app/companionWindow"; import { useAgentsDataRefresh } from "@/features/agents/lib/useAgentsDataRefresh"; import { useManagedAgentRuntimeReconciliation } from "@/features/agents/useManagedAgentRuntimeReconciliation"; import { useAutoRestartPolicy } from "@/features/agents/lib/useAutoRestartPolicy"; @@ -126,8 +126,9 @@ export function AppShell() { showHuddleInMainApp, viewHuddleChannel, } = useHuddlePresentation(); - const isActivityWindow = isAgentActivityWindow(); - const isCompanionWindow = isHuddleRoom || isActivityWindow; + const companionWindowKind = currentCompanionWindowKind(); + const isActivityWindow = companionWindowKind === "agent-activity"; + const isCompanionWindow = companionWindowKind !== null; const hasCommunityRail = communitiesHook.communities.length > 1 && !isCompanionWindow; const addCommunityDialog = useAddCommunityDialogState(); @@ -354,7 +355,7 @@ export function AppShell() { handleThreadReplyDesktopNotification, } = useAppShellDesktopNotifications({ channels, - enabled: !isHuddleRoom, + enabled: !isCompanionWindow, goChannel, goHome, notificationSettings: notificationSettings.settings, @@ -391,8 +392,8 @@ export function AppShell() { muteThread, unmuteThread, } = useUnreadChannels( - isHuddleRoom ? EMPTY_CHANNELS : sidebarChannels, - isHuddleRoom ? null : activeChannel, + isCompanionWindow ? EMPTY_CHANNELS : sidebarChannels, + isCompanionWindow ? null : activeChannel, { pubkey: identityQuery.data?.pubkey, relayClient, @@ -454,7 +455,7 @@ export function AppShell() { identityQuery.data?.pubkey, notificationSettings.settings, notificationSettings.setDesktopEnabled, - !isHuddleRoom, + !isCompanionWindow, selectedView === "home" && !settingsOpen, getChannelReadAt, readStateVersion, @@ -658,13 +659,13 @@ export function AppShell() { [openSearchHit], ); useAppShellLifecycleEffects({ - desktopBadgeEnabled: !isHuddleRoom, + desktopBadgeEnabled: !isCompanionWindow, homeBadgeCountExcludingHighPriority, topLevelUnreadChannelIds, unreadChannelNotificationCount, }); - // Dispatch `buzz://` deep links only from the main window; the companion is dedicated to its active Huddle route. - useAppDeepLinks(!isHuddleRoom); + // Dispatch `buzz://` deep links only from the primary window; companions own their focused route. + useAppDeepLinks(!isCompanionWindow); const handleOpenCreateChannel = React.useCallback( () => setIsCreateChannelOpen(true), [], @@ -673,7 +674,7 @@ export function AppShell() { activeChannelId: selectedView === "channel" ? selectedChannelId : null, canSearchCurrentChannel: selectedView === "channel" && Boolean(activeChannel), - disabled: settingsOpen || isHuddleRoom, + disabled: settingsOpen || isCompanionWindow, onBrowseChannels: handleOpenBrowseChannels, onCreateChannel: handleOpenCreateChannel, onGoHome: goHome, @@ -684,7 +685,7 @@ export function AppShell() { useSettingsShortcuts({ onClose: handleCloseSettings, onOpenSettings: handleOpenSettings, - open: isHuddleRoom ? undefined : settingsOpen, + open: isCompanionWindow ? undefined : settingsOpen, }); useMarkAsReadShortcuts({ activeChannelId: activeChannel?.id ?? null, diff --git a/desktop/src/app/companionWindow.test.mjs b/desktop/src/app/companionWindow.test.mjs new file mode 100644 index 0000000000..cec4b4cc0c --- /dev/null +++ b/desktop/src/app/companionWindow.test.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { companionWindowKindForLabel } from "./companionWindow.ts"; + +describe("companionWindowKindForLabel", () => { + it("classifies focused companion labels", () => { + assert.equal( + companionWindowKindForLabel("agent-activity-deadbeef-channel"), + "agent-activity", + ); + assert.equal(companionWindowKindForLabel("huddle-channel-id"), "huddle"); + }); + + it("leaves primary and unrelated windows unclassified", () => { + assert.equal(companionWindowKindForLabel("main"), null); + assert.equal(companionWindowKindForLabel("reader-document"), null); + }); +}); diff --git a/desktop/src/app/companionWindow.ts b/desktop/src/app/companionWindow.ts new file mode 100644 index 0000000000..1204870f91 --- /dev/null +++ b/desktop/src/app/companionWindow.ts @@ -0,0 +1,30 @@ +import { isTauri } from "@tauri-apps/api/core"; +import { getCurrentWindow } from "@tauri-apps/api/window"; + +export type CompanionWindowKind = "agent-activity" | "huddle"; + +/** Classify native companion surfaces that reuse the main application shell. */ +export function companionWindowKindForLabel( + label: string, +): CompanionWindowKind | null { + if (label.startsWith("agent-activity-")) return "agent-activity"; + if (label.startsWith("huddle-")) return "huddle"; + return null; +} + +/** Return this webview's companion kind, or null for the primary app window. */ +export function currentCompanionWindowKind(): CompanionWindowKind | null { + if (!isTauri()) return null; + + try { + return companionWindowKindForLabel(getCurrentWindow().label); + } catch { + // Browser previews can expose the Tauri IPC mock without window metadata. + return null; + } +} + +/** Whether this webview is a focused companion rather than the primary app. */ +export function isCompanionWindow(): boolean { + return currentCompanionWindowKind() !== null; +} diff --git a/desktop/src/features/agents/lib/agentActivityWindow.ts b/desktop/src/features/agents/lib/agentActivityWindow.ts index ffcf2ac324..fb91aa7caf 100644 --- a/desktop/src/features/agents/lib/agentActivityWindow.ts +++ b/desktop/src/features/agents/lib/agentActivityWindow.ts @@ -1,19 +1,9 @@ -import { isTauri } from "@tauri-apps/api/core"; +import { currentCompanionWindowKind } from "@/app/companionWindow"; import { getCurrentWindow } from "@tauri-apps/api/window"; -const AGENT_ACTIVITY_WINDOW_LABEL_PREFIX = "agent-activity-"; - /** Whether this webview is dedicated to an agent activity feed. */ export function isAgentActivityWindow(): boolean { - if (!isTauri()) return false; - - try { - return getCurrentWindow().label.startsWith( - AGENT_ACTIVITY_WINDOW_LABEL_PREFIX, - ); - } catch { - return false; - } + return currentCompanionWindowKind() === "agent-activity"; } /** Build the concise native title for a channel-scoped activity window. */ diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx index 777861fa39..484693d4e0 100644 --- a/desktop/src/main.tsx +++ b/desktop/src/main.tsx @@ -11,8 +11,7 @@ import "@/shared/styles/globals.css"; import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider"; import { migrateLegacyCommunityStorageBeforeRender } from "@/features/communities/legacyCommunityStorage"; import { CommunitiesProvider } from "@/features/communities/useCommunities"; -import { huddleWindowChannelId } from "@/features/huddle/lib/huddleWindow"; -import { isAgentActivityWindow } from "@/features/agents/lib/agentActivityWindow"; +import { isCompanionWindow } from "@/app/companionWindow"; import { CommunityOnboardingProvider } from "@/features/onboarding/communityOnboarding"; import { ThemeProvider } from "@/shared/theme/ThemeProvider"; import { EmojiBurstProvider } from "@/shared/ui/EmojiBurstProvider"; @@ -86,11 +85,7 @@ function renderApp() { SecurityError from localStorage can't blank the whole window. */} - +