From 6285ecb2db38b03105c38a0a615ee37c52036a67 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 04:27:21 +0000 Subject: [PATCH 1/5] feat: force pin listening tab and stop on close - Auto-pin session tab when listening starts - Allow closing active listening tabs (removed canClose restriction) - Stop listening and trigger auto-enhance when closing an active session tab Co-Authored-By: john@hyprnote.com --- apps/desktop/src/routes/app/main/_layout.tsx | 41 +++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/routes/app/main/_layout.tsx b/apps/desktop/src/routes/app/main/_layout.tsx index de6d9a57c0..000ac1edba 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,20 @@ function Component() { const { persistedStore, aiTaskStore, toolRegistry } = useRouteContext({ from: "__root__", }); - const { registerOnEmpty, registerCanClose, openNew, tabs } = useTabs(); + const { + registerOnEmpty, + registerCanClose, + registerOnClose, + openNew, + tabs, + pin, + } = useTabs(); 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 +60,33 @@ function Component() { }, [tabs.length, openDefaultEmptyTab, registerOnEmpty]); useEffect(() => { - registerCanClose((tab) => { + const justStartedListening = + prevLiveStatus !== "active" && liveStatus === "active"; + if (justStartedListening && liveSessionId) { + const sessionTab = tabs.find( + (t) => t.type === "sessions" && t.id === liveSessionId, + ); + if (sessionTab && !sessionTab.pinned) { + pin(sessionTab); + } + } + }, [liveStatus, prevLiveStatus, liveSessionId, tabs, 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; From 9dee38e0d1a3e6aba74a72137be454e29fc687e9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:18:41 +0000 Subject: [PATCH 2/5] feat: move listening tab to fixed section on far left - Listening tab is now rendered in a separate fixed section before navigation buttons - Tab is always visible when actively listening - Filtered out from the regular reorderable tab group Co-Authored-By: john@hyprnote.com --- .../src/components/main/body/index.tsx | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index f5008026cc..8b0d8a0a09 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,28 @@ 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]); - const setTabRef = useScrollActiveTabIntoView(tabs); + const setTabRef = useScrollActiveTabIntoView(regularTabs); useTabsShortcuts(); return ( @@ -139,6 +153,21 @@ function Header({ tabs }: { tabs: Tab[] }) { )} + {listeningTab && ( +
+ +
+ )} +
)} - {listeningTab && ( -
- -
- )} -
+ {listeningTab && ( +
+ +
+ )} +
Date: Sun, 11 Jan 2026 03:43:27 +0900 Subject: [PATCH 4/5] refactor(tabs): optimize tabs state management --- apps/desktop/src/routes/app/main/_layout.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/routes/app/main/_layout.tsx b/apps/desktop/src/routes/app/main/_layout.tsx index 000ac1edba..cf65286b3c 100644 --- a/apps/desktop/src/routes/app/main/_layout.tsx +++ b/apps/desktop/src/routes/app/main/_layout.tsx @@ -29,14 +29,9 @@ function Component() { const { persistedStore, aiTaskStore, toolRegistry } = useRouteContext({ from: "__root__", }); - const { - registerOnEmpty, - registerCanClose, - registerOnClose, - openNew, - tabs, - pin, - } = 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); @@ -63,14 +58,15 @@ function Component() { const justStartedListening = prevLiveStatus !== "active" && liveStatus === "active"; if (justStartedListening && liveSessionId) { - const sessionTab = tabs.find( + 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, tabs, pin]); + }, [liveStatus, prevLiveStatus, liveSessionId, pin]); useEffect(() => { registerOnClose((tab) => { From 9b9dd2a946d5573ad2fd23505c4215028e6bd97c Mon Sep 17 00:00:00 2001 From: John Jeong Date: Sun, 11 Jan 2026 03:48:14 +0900 Subject: [PATCH 5/5] refactor(tabs): optimize scroll state and active tab handling --- .../src/components/main/body/index.tsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index db4aa066b4..f47909303d 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -125,7 +125,10 @@ function Header({ tabs }: { tabs: Tab[] }) { useState(false); const { ref: rightContainerRef, hasSpace: hasSpaceForSearch } = useHasSpaceForSearch(); - const scrollState = useScrollState(tabsScrollContainerRef, [regularTabs]); + const scrollState = useScrollState( + tabsScrollContainerRef, + regularTabs.length, + ); const setTabRef = useScrollActiveTabIntoView(regularTabs); useTabsShortcuts(); @@ -604,7 +607,7 @@ function useHasSpaceForSearch() { function useScrollState( ref: React.RefObject, - deps: unknown[] = [], + tabCount: number, ) { const [scrollState, setScrollState] = useState({ atStart: true, @@ -616,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]); @@ -637,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", @@ -658,7 +667,7 @@ function useScrollActiveTabIntoView(tabs: Tab[]) { }); } } - }, [tabs]); + }, [activeTabKey]); const setTabRef = useCallback((tab: Tab, el: HTMLDivElement | null) => { if (el) {