diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index f5008026cc..f47909303d 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -14,6 +14,7 @@ import { useShallow } from "zustand/shallow"; import { Button } from "@hypr/ui/components/ui/button"; import { cn } from "@hypr/utils"; +import { useListener } from "../../../contexts/listener"; import { useNotifications } from "../../../contexts/notifications"; import { useShell } from "../../../contexts/shell"; import { @@ -105,15 +106,31 @@ function Header({ tabs }: { tabs: Tab[] }) { unpin: state.unpin, })), ); + + const liveSessionId = useListener((state) => state.live.sessionId); + const liveStatus = useListener((state) => state.live.status); + const isListening = liveStatus === "active" || liveStatus === "finalizing"; + + const listeningTab = + isListening && liveSessionId + ? tabs.find((t) => t.type === "sessions" && t.id === liveSessionId) + : null; + const regularTabs = listeningTab + ? tabs.filter((t) => !(t.type === "sessions" && t.id === liveSessionId)) + : tabs; + const tabsScrollContainerRef = useRef(null); const handleNewEmptyTab = useNewEmptyTab(); const [isSearchManuallyExpanded, setIsSearchManuallyExpanded] = useState(false); const { ref: rightContainerRef, hasSpace: hasSpaceForSearch } = useHasSpaceForSearch(); - const scrollState = useScrollState(tabsScrollContainerRef, [tabs]); + const scrollState = useScrollState( + tabsScrollContainerRef, + regularTabs.length, + ); - const setTabRef = useScrollActiveTabIntoView(tabs); + const setTabRef = useScrollActiveTabIntoView(regularTabs); useTabsShortcuts(); return ( @@ -158,6 +175,21 @@ function Header({ tabs }: { tabs: Tab[] }) { + {listeningTab && ( +
+ +
+ )} +
- {tabs.map((tab, index) => { - const isLastTab = index === tabs.length - 1; - const shortcutIndex = - index < 8 ? index + 1 : isLastTab ? 9 : undefined; + {regularTabs.map((tab, index) => { + const isLastTab = index === regularTabs.length - 1; + const shortcutIndex = listeningTab + ? index < 7 + ? index + 2 + : isLastTab + ? 9 + : undefined + : index < 8 + ? index + 1 + : isLastTab + ? 9 + : undefined; return ( , - deps: unknown[] = [], + tabCount: number, ) { const [scrollState, setScrollState] = useState({ atStart: true, @@ -578,9 +619,15 @@ function useScrollState( if (!container) return; const { scrollLeft, scrollWidth, clientWidth } = container; - setScrollState({ + const newState = { atStart: scrollLeft <= 1, atEnd: scrollLeft + clientWidth >= scrollWidth - 1, + }; + setScrollState((prev) => { + if (prev.atStart === newState.atStart && prev.atEnd === newState.atEnd) { + return prev; + } + return newState; }); }, [ref]); @@ -599,19 +646,19 @@ function useScrollState( return () => { container.removeEventListener("scroll", updateScrollState); }; - }, [updateScrollState, ...deps]); + }, [updateScrollState, tabCount]); return scrollState; } function useScrollActiveTabIntoView(tabs: Tab[]) { const tabRefsMap = useRef>(new Map()); + const activeTab = tabs.find((tab) => tab.active); + const activeTabKey = activeTab ? uniqueIdfromTab(activeTab) : null; useEffect(() => { - const activeTab = tabs.find((tab) => tab.active); - if (activeTab) { - const tabKey = uniqueIdfromTab(activeTab); - const tabElement = tabRefsMap.current.get(tabKey); + if (activeTabKey) { + const tabElement = tabRefsMap.current.get(activeTabKey); if (tabElement) { tabElement.scrollIntoView({ behavior: "smooth", @@ -620,7 +667,7 @@ function useScrollActiveTabIntoView(tabs: Tab[]) { }); } } - }, [tabs]); + }, [activeTabKey]); const setTabRef = useCallback((tab: Tab, el: HTMLDivElement | null) => { if (el) { diff --git a/apps/desktop/src/routes/app/main/_layout.tsx b/apps/desktop/src/routes/app/main/_layout.tsx index de6d9a57c0..cf65286b3c 100644 --- a/apps/desktop/src/routes/app/main/_layout.tsx +++ b/apps/desktop/src/routes/app/main/_layout.tsx @@ -3,6 +3,7 @@ import { Outlet, useRouteContext, } from "@tanstack/react-router"; +import { usePrevious } from "@uidotdev/usehooks"; import { useCallback, useEffect, useRef } from "react"; import { events as deeplink2Events } from "@hypr/plugin-deeplink2"; @@ -28,9 +29,15 @@ function Component() { const { persistedStore, aiTaskStore, toolRegistry } = useRouteContext({ from: "__root__", }); - const { registerOnEmpty, registerCanClose, openNew, tabs } = useTabs(); + const { registerOnEmpty, registerCanClose, registerOnClose, openNew, pin } = + useTabs(); + const tabs = useTabs((state) => state.tabs); const hasOpenedInitialTab = useRef(false); + const liveSessionId = useListener((state) => state.live.sessionId); + const liveStatus = useListener((state) => state.live.status); + const prevLiveStatus = usePrevious(liveStatus); const getSessionMode = useListener((state) => state.getSessionMode); + const stop = useListener((state) => state.stop); useDeeplinkHandler(); @@ -48,14 +55,34 @@ function Component() { }, [tabs.length, openDefaultEmptyTab, registerOnEmpty]); useEffect(() => { - registerCanClose((tab) => { + const justStartedListening = + prevLiveStatus !== "active" && liveStatus === "active"; + if (justStartedListening && liveSessionId) { + const currentTabs = useTabs.getState().tabs; + const sessionTab = currentTabs.find( + (t) => t.type === "sessions" && t.id === liveSessionId, + ); + if (sessionTab && !sessionTab.pinned) { + pin(sessionTab); + } + } + }, [liveStatus, prevLiveStatus, liveSessionId, pin]); + + useEffect(() => { + registerOnClose((tab) => { if (tab.type !== "sessions") { - return true; + return; } const mode = getSessionMode(tab.id); - return mode !== "active" && mode !== "finalizing"; + if (mode === "active" || mode === "finalizing") { + stop(); + } }); - }, [registerCanClose, getSessionMode]); + }, [registerOnClose, getSessionMode, stop]); + + useEffect(() => { + registerCanClose(() => true); + }, [registerCanClose]); if (!aiTaskStore) { return null;