diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index ed7ef5c999..5b602f501d 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -91,6 +91,8 @@ function Header({ tabs }: { tabs: Tab[] }) { closeAll, pin, unpin, + pendingCloseConfirmationTab, + setPendingCloseConfirmationTab, } = useTabs( useShallow((state) => ({ select: state.select, @@ -104,6 +106,8 @@ function Header({ tabs }: { tabs: Tab[] }) { closeAll: state.closeAll, pin: state.pin, unpin: state.unpin, + pendingCloseConfirmationTab: state.pendingCloseConfirmationTab, + setPendingCloseConfirmationTab: state.setPendingCloseConfirmationTab, })), ); @@ -193,6 +197,8 @@ function Header({ tabs }: { tabs: Tab[] }) { handlePin={pin} handleUnpin={unpin} tabIndex={1} + pendingCloseConfirmationTab={pendingCloseConfirmationTab} + setPendingCloseConfirmationTab={setPendingCloseConfirmationTab} /> )} @@ -247,6 +253,10 @@ function Header({ tabs }: { tabs: Tab[] }) { handlePin={pin} handleUnpin={unpin} tabIndex={shortcutIndex} + pendingCloseConfirmationTab={pendingCloseConfirmationTab} + setPendingCloseConfirmationTab={ + setPendingCloseConfirmationTab + } /> ); @@ -298,6 +308,8 @@ function TabItem({ handlePin, handleUnpin, tabIndex, + pendingCloseConfirmationTab, + setPendingCloseConfirmationTab, }: { tab: Tab; handleClose: (tab: Tab) => void; @@ -307,6 +319,8 @@ function TabItem({ handlePin: (tab: Tab) => void; handleUnpin: (tab: Tab) => void; tabIndex?: number; + pendingCloseConfirmationTab?: Tab | null; + setPendingCloseConfirmationTab?: (tab: Tab | null) => void; }) { const handleCloseOthers = () => handleCloseOthersCallback(tab); const handlePinThis = () => handlePin(tab); @@ -323,6 +337,8 @@ function TabItem({ handleCloseAll={handleCloseAll} handlePinThis={handlePinThis} handleUnpinThis={handleUnpinThis} + pendingCloseConfirmationTab={pendingCloseConfirmationTab} + setPendingCloseConfirmationTab={setPendingCloseConfirmationTab} /> ); } @@ -716,6 +732,7 @@ function useTabsShortcuts() { restoreLastClosedTab, openNew, unpin, + setPendingCloseConfirmationTab, } = useTabs( useShallow((state) => ({ tabs: state.tabs, @@ -727,8 +744,13 @@ function useTabsShortcuts() { restoreLastClosedTab: state.restoreLastClosedTab, openNew: state.openNew, unpin: state.unpin, + setPendingCloseConfirmationTab: state.setPendingCloseConfirmationTab, })), ); + const liveSessionId = useListener((state) => state.live.sessionId); + const liveStatus = useListener((state) => state.live.status); + const isListening = liveStatus === "active" || liveStatus === "finalizing"; + const newNote = useNewNote({ behavior: "new" }); const newNoteCurrent = useNewNote({ behavior: "current" }); const newEmptyTab = useNewEmptyTab(); @@ -765,7 +787,13 @@ function useTabsShortcuts() { "mod+w", async () => { if (currentTab) { - if (currentTab.pinned) { + const isCurrentTabListening = + isListening && + currentTab.type === "sessions" && + currentTab.id === liveSessionId; + if (isCurrentTabListening) { + setPendingCloseConfirmationTab(currentTab); + } else if (currentTab.pinned) { unpin(currentTab); } else { close(currentTab); @@ -777,7 +805,14 @@ function useTabsShortcuts() { enableOnFormTags: true, enableOnContentEditable: true, }, - [currentTab, close, unpin], + [ + currentTab, + close, + unpin, + isListening, + liveSessionId, + setPendingCloseConfirmationTab, + ], ); useHotkeys( diff --git a/apps/desktop/src/components/main/body/sessions/index.tsx b/apps/desktop/src/components/main/body/sessions/index.tsx index 503ec8d879..248efa7064 100644 --- a/apps/desktop/src/components/main/body/sessions/index.tsx +++ b/apps/desktop/src/components/main/body/sessions/index.tsx @@ -17,6 +17,7 @@ import { useStartListening } from "../../../../hooks/useStartListening"; import { useSTTConnection } from "../../../../hooks/useSTTConnection"; import { useTitleGeneration } from "../../../../hooks/useTitleGeneration"; import * as main from "../../../../store/tinybase/store/main"; +import { listenerStore } from "../../../../store/zustand/listener/instance"; import { rowIdfromTab, type Tab, @@ -48,6 +49,8 @@ export const TabItemNote: TabItem> = ({ handleCloseAll, handlePinThis, handleUnpinThis, + pendingCloseConfirmationTab, + setPendingCloseConfirmationTab, }) => { const title = main.UI.useCell( "sessions", @@ -56,11 +59,36 @@ export const TabItemNote: TabItem> = ({ main.STORE_ID, ); const sessionMode = useListener((state) => state.getSessionMode(tab.id)); + const stop = useListener((state) => state.stop); const isEnhancing = useIsSessionEnhancing(tab.id); const isActive = sessionMode === "active" || sessionMode === "finalizing"; const isFinalizing = sessionMode === "finalizing"; const showSpinner = !tab.active && (isFinalizing || isEnhancing); + const showCloseConfirmation = + pendingCloseConfirmationTab?.type === "sessions" && + pendingCloseConfirmationTab?.id === tab.id; + + const handleCloseConfirmationChange = (show: boolean) => { + if (!show) { + setPendingCloseConfirmationTab?.(null); + } + }; + + const handleCloseWithStop = () => { + if (isActive) { + stop(); + const unsubscribe = listenerStore.subscribe((state) => { + if (state.live.status !== "active") { + unsubscribe(); + handleCloseThis(tab); + } + }); + } else { + handleCloseThis(tab); + } + }; + return ( } @@ -70,7 +98,9 @@ export const TabItemNote: TabItem> = ({ finalizing={showSpinner} pinned={tab.pinned} tabIndex={tabIndex} - handleCloseThis={() => handleCloseThis(tab)} + showCloseConfirmation={showCloseConfirmation} + onCloseConfirmationChange={handleCloseConfirmationChange} + handleCloseThis={handleCloseWithStop} handleSelectThis={() => handleSelectThis(tab)} handleCloseOthers={handleCloseOthers} handleCloseAll={handleCloseAll} diff --git a/apps/desktop/src/components/main/body/shared.tsx b/apps/desktop/src/components/main/body/shared.tsx index 027475006c..5632eed45b 100644 --- a/apps/desktop/src/components/main/body/shared.tsx +++ b/apps/desktop/src/components/main/body/shared.tsx @@ -1,7 +1,13 @@ import { Pin, X } from "lucide-react"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { Button } from "@hypr/ui/components/ui/button"; import { Kbd } from "@hypr/ui/components/ui/kbd"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@hypr/ui/components/ui/popover"; import { Spinner } from "@hypr/ui/components/ui/spinner"; import { cn } from "@hypr/utils"; @@ -16,6 +22,8 @@ type TabItemProps = { tab: T; tabIndex?: number } & { handleCloseAll: () => void; handlePinThis: (tab: T) => void; handleUnpinThis: (tab: T) => void; + pendingCloseConfirmationTab?: Tab | null; + setPendingCloseConfirmationTab?: (tab: Tab | null) => void; }; type TabItemBaseProps = { @@ -28,6 +36,8 @@ type TabItemBaseProps = { allowPin?: boolean; isEmptyTab?: boolean; tabIndex?: number; + showCloseConfirmation?: boolean; + onCloseConfirmationChange?: (show: boolean) => void; } & { handleCloseThis: () => void; handleSelectThis: () => void; @@ -51,6 +61,8 @@ export function TabItemBase({ allowPin = true, isEmptyTab = false, tabIndex, + showCloseConfirmation = false, + onCloseConfirmationChange, handleCloseThis, handleSelectThis, handleCloseOthers, @@ -60,6 +72,51 @@ export function TabItemBase({ }: TabItemBaseProps) { const isCmdPressed = useCmdKeyPressed(); const [isHovered, setIsHovered] = useState(false); + const [localShowConfirmation, setLocalShowConfirmation] = useState(false); + + const isConfirmationOpen = showCloseConfirmation || localShowConfirmation; + + useEffect(() => { + if (showCloseConfirmation) { + setLocalShowConfirmation(true); + } + }, [showCloseConfirmation]); + + const handleCloseConfirmationChange = (open: boolean) => { + setLocalShowConfirmation(open); + onCloseConfirmationChange?.(open); + }; + + const handleAttemptClose = () => { + if (active) { + handleCloseConfirmationChange(true); + } else { + handleCloseThis(); + } + }; + + const handleConfirmClose = useCallback(() => { + setLocalShowConfirmation(false); + onCloseConfirmationChange?.(false); + handleCloseThis(); + }, [handleCloseThis, onCloseConfirmationChange]); + + useEffect(() => { + if (!isConfirmationOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "w") { + e.preventDefault(); + e.stopPropagation(); + handleConfirmClose(); + } + }; + + window.addEventListener("keydown", handleKeyDown, { capture: true }); + return () => { + window.removeEventListener("keydown", handleKeyDown, { capture: true }); + }; + }, [isConfirmationOpen, handleConfirmClose]); const handleMouseDown = (e: React.MouseEvent) => { if (e.button === 1 && !active) { @@ -72,7 +129,7 @@ export function TabItemBase({ const contextMenu = active || (selected && !isEmptyTab) ? [ - { id: "close-tab", text: "Close", action: handleCloseThis }, + { id: "close-tab", text: "Close", action: handleAttemptClose }, ...(allowPin ? [ { separator: true as const }, @@ -87,7 +144,7 @@ export function TabItemBase({ : []), ] : [ - { id: "close-tab", text: "Close", action: handleCloseThis }, + { id: "close-tab", text: "Close", action: handleAttemptClose }, { id: "close-others", text: "Close others", @@ -114,7 +171,7 @@ export function TabItemBase({
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} - className="h-full" + className="h-full relative" > {finalizing ? ( @@ -176,13 +233,13 @@ export function TabItemBase({
)}
+ + +
+ + e.preventDefault()} + > +
+

+ Are you sure you want to close this tab? This will stop Hyprnote + from listening. +

+ +
+
+
); } diff --git a/apps/desktop/src/hooks/useAutoEnhance.ts b/apps/desktop/src/hooks/useAutoEnhance.ts index af4094b349..d48ed1dd87 100644 --- a/apps/desktop/src/hooks/useAutoEnhance.ts +++ b/apps/desktop/src/hooks/useAutoEnhance.ts @@ -22,6 +22,8 @@ export function useAutoEnhance(tab: Extract) { const listenerStatus = useListener((state) => state.live.status); const prevListenerStatus = usePrevious(listenerStatus); + const liveSessionId = useListener((state) => state.live.sessionId); + const prevLiveSessionId = usePrevious(liveSessionId); const indexes = main.UI.useIndexes(main.STORE_ID); @@ -162,11 +164,18 @@ export function useAutoEnhance(tab: Extract) { useEffect(() => { const listenerJustStopped = prevListenerStatus === "active" && listenerStatus !== "active"; + const wasThisSessionListening = prevLiveSessionId === sessionId; - if (listenerJustStopped) { + if (listenerJustStopped && wasThisSessionListening) { createAndStartEnhance(); } - }, [listenerStatus, prevListenerStatus, createAndStartEnhance]); + }, [ + listenerStatus, + prevListenerStatus, + prevLiveSessionId, + sessionId, + createAndStartEnhance, + ]); useEffect(() => { if (listenerStatus === "finalizing" && indexes) { diff --git a/apps/desktop/src/routes/app/main/_layout.tsx b/apps/desktop/src/routes/app/main/_layout.tsx index 75d3fb1dee..e70e725089 100644 --- a/apps/desktop/src/routes/app/main/_layout.tsx +++ b/apps/desktop/src/routes/app/main/_layout.tsx @@ -27,15 +27,12 @@ function Component() { const { persistedStore, aiTaskStore, toolRegistry } = useRouteContext({ from: "__root__", }); - const { registerOnEmpty, registerCanClose, registerOnClose, openNew, pin } = - useTabs(); + const { registerOnEmpty, registerCanClose, 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(); @@ -66,18 +63,6 @@ function Component() { } }, [liveStatus, prevLiveStatus, liveSessionId, pin]); - useEffect(() => { - registerOnClose((tab) => { - if (tab.type !== "sessions") { - return; - } - const mode = getSessionMode(tab.id); - if (mode === "active" || mode === "finalizing") { - stop(); - } - }); - }, [registerOnClose, getSessionMode, stop]); - useEffect(() => { registerCanClose(() => true); }, [registerCanClose]); diff --git a/apps/desktop/src/store/zustand/tabs/lifecycle.ts b/apps/desktop/src/store/zustand/tabs/lifecycle.ts index 40e78c1841..e19e682226 100644 --- a/apps/desktop/src/store/zustand/tabs/lifecycle.ts +++ b/apps/desktop/src/store/zustand/tabs/lifecycle.ts @@ -6,12 +6,14 @@ export type LifecycleState = { onClose: ((tab: Tab) => void) | null; onEmpty: (() => void) | null; canClose: ((tab: Tab) => boolean) | null; + pendingCloseConfirmationTab: Tab | null; }; export type LifecycleActions = { registerOnClose: (handler: (tab: Tab) => void) => void; registerOnEmpty: (handler: () => void) => void; registerCanClose: (handler: (tab: Tab) => boolean) => void; + setPendingCloseConfirmationTab: (tab: Tab | null) => void; }; export const createLifecycleSlice = ( @@ -21,9 +23,12 @@ export const createLifecycleSlice = ( onClose: null, onEmpty: null, canClose: null, + pendingCloseConfirmationTab: null, registerOnClose: (handler) => set({ onClose: handler } as Partial), registerOnEmpty: (handler) => set({ onEmpty: handler } as Partial), registerCanClose: (handler) => set({ canClose: handler } as Partial), + setPendingCloseConfirmationTab: (tab) => + set({ pendingCloseConfirmationTab: tab } as Partial), }); type LifecycleMiddleware = <