diff --git a/apps/desktop/src/components/main/body/sessions/index.tsx b/apps/desktop/src/components/main/body/sessions/index.tsx index 0b0eac1537..70d381906c 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 { useSessionTitle } from "../../../../store/zustand/live-title"; import { type Tab, useTabs } from "../../../../store/zustand/tabs"; import { StandardTabWrapper } from "../index"; import { type TabItem, TabItemBase } from "../shared"; @@ -43,7 +44,13 @@ export const TabItemNote: TabItem> = ({ pendingCloseConfirmationTab, setPendingCloseConfirmationTab, }) => { - const title = main.UI.useCell("sessions", tab.id, "title", main.STORE_ID); + const storeTitle = main.UI.useCell( + "sessions", + tab.id, + "title", + main.STORE_ID, + ); + const title = useSessionTitle(tab.id, storeTitle as string | undefined); const sessionMode = useListener((state) => state.getSessionMode(tab.id)); const stop = useListener((state) => state.stop); const isEnhancing = useIsSessionEnhancing(tab.id); diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/folder.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/folder.tsx index a9d2933a59..4da0b0df31 100644 --- a/apps/desktop/src/components/main/body/sessions/outer-header/folder.tsx +++ b/apps/desktop/src/components/main/body/sessions/outer-header/folder.tsx @@ -11,6 +11,7 @@ import { import { Button } from "@hypr/ui/components/ui/button"; import * as main from "../../../../../store/tinybase/store/main"; +import { useSessionTitle } from "../../../../../store/zustand/live-title"; import { useTabs } from "../../../../../store/zustand/tabs"; import { FolderBreadcrumb } from "../../shared/folder-breadcrumb"; import { SearchableFolderDropdown } from "./shared/folder"; @@ -22,9 +23,13 @@ export function FolderChain({ sessionId }: { sessionId: string }) { "folder_id", main.STORE_ID, ); - const title = - main.UI.useCell("sessions", sessionId, "title", main.STORE_ID) ?? - "Untitled"; + const storeTitle = main.UI.useCell( + "sessions", + sessionId, + "title", + main.STORE_ID, + ) as string | undefined; + const title = useSessionTitle(sessionId, storeTitle); const handleChangeTitle = main.UI.useSetPartialRowCallback( "sessions", diff --git a/apps/desktop/src/components/main/body/sessions/title-input.tsx b/apps/desktop/src/components/main/body/sessions/title-input.tsx index 85c63ee683..535e15b281 100644 --- a/apps/desktop/src/components/main/body/sessions/title-input.tsx +++ b/apps/desktop/src/components/main/body/sessions/title-input.tsx @@ -19,6 +19,7 @@ import { cn } from "@hypr/utils"; import { useTitleGenerating } from "../../../../hooks/useTitleGenerating"; import * as main from "../../../../store/tinybase/store/main"; +import { useLiveTitle } from "../../../../store/zustand/live-title"; import { type Tab } from "../../../../store/zustand/tabs"; export const TitleInput = forwardRef< @@ -119,6 +120,8 @@ const TitleInputInner = memo( const isFocused = useRef(false); const internalRef = useRef(null); const store = main.UI.useStore(main.STORE_ID); + const setLiveTitle = useLiveTitle((s) => s.setTitle); + const clearLiveTitle = useLiveTitle((s) => s.clearTitle); useImperativeHandle(ref, () => internalRef.current!, []); @@ -149,37 +152,6 @@ const TitleInputInner = memo( main.STORE_ID, ); - const debounceRef = useRef | null>(null); - const pendingValueRef = useRef(null); - - const flushDebounce = useCallback(() => { - if (debounceRef.current) { - clearTimeout(debounceRef.current); - debounceRef.current = null; - } - if (pendingValueRef.current !== null) { - setStoreTitle(pendingValueRef.current); - pendingValueRef.current = null; - } - }, [setStoreTitle]); - - const debouncedPersist = useCallback( - (value: string) => { - pendingValueRef.current = value; - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - debounceRef.current = setTimeout(() => { - if (pendingValueRef.current !== null) { - setStoreTitle(pendingValueRef.current); - pendingValueRef.current = null; - } - debounceRef.current = null; - }, 150); - }, - [setStoreTitle], - ); - useEffect(() => { const handleMoveToTitlePosition = (e: Event) => { const customEvent = e as CustomEvent<{ pixelWidth: number }>; @@ -238,8 +210,8 @@ const TitleInputInner = memo( const beforeCursor = input.value.slice(0, cursorPos); const afterCursor = input.value.slice(cursorPos); - flushDebounce(); setStoreTitle(beforeCursor); + clearLiveTitle(sessionId); if (afterCursor) { setTimeout(() => { @@ -298,8 +270,9 @@ const TitleInputInner = memo( placeholder="Untitled" type="text" onChange={(e) => { - setLocalTitle(e.target.value); - debouncedPersist(e.target.value); + const value = e.target.value; + setLocalTitle(value); + setLiveTitle(sessionId, value); }} onKeyDown={handleKeyDown} onFocus={() => { @@ -307,7 +280,8 @@ const TitleInputInner = memo( }} onBlur={() => { isFocused.current = false; - flushDebounce(); + setStoreTitle(localTitle); + clearLiveTitle(sessionId); }} value={localTitle} className={cn([ diff --git a/apps/desktop/src/components/main/sidebar/timeline/item.tsx b/apps/desktop/src/components/main/sidebar/timeline/item.tsx index 054d359b6f..121da8814f 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/item.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/item.tsx @@ -20,6 +20,7 @@ import { import * as main from "../../../../store/tinybase/store/main"; import { save } from "../../../../store/tinybase/store/save"; import { getOrCreateSessionForEventId } from "../../../../store/tinybase/store/sessions"; +import { useSessionTitle } from "../../../../store/zustand/live-title"; import { type TabInput, useTabs } from "../../../../store/zustand/tabs"; import { useTimelineSelection } from "../../../../store/zustand/timeline-selection"; import { useUndoDelete } from "../../../../store/zustand/undo-delete"; @@ -127,7 +128,7 @@ function ItemBase({ ignored && "line-through", )} > - {title} + {title || Untitled} {displayTime && (
{displayTime}
@@ -323,10 +324,13 @@ const SessionItem = memo( const addDeletion = useUndoDelete((state) => state.addDeletion); const sessionId = item.id; - const title = - (main.UI.useCell("sessions", sessionId, "title", main.STORE_ID) as - | string - | undefined) || "Untitled"; + const storeTitle = main.UI.useCell( + "sessions", + sessionId, + "title", + main.STORE_ID, + ) as string | undefined; + const title = useSessionTitle(sessionId, storeTitle); const sessionMode = useListener((state) => state.getSessionMode(sessionId)); const isEnhancing = useIsSessionEnhancing(sessionId); diff --git a/apps/desktop/src/store/zustand/live-title.ts b/apps/desktop/src/store/zustand/live-title.ts new file mode 100644 index 0000000000..1bebd51b1c --- /dev/null +++ b/apps/desktop/src/store/zustand/live-title.ts @@ -0,0 +1,26 @@ +import { create } from "zustand"; + +interface LiveTitleState { + titles: Record; + setTitle: (sessionId: string, title: string) => void; + clearTitle: (sessionId: string) => void; +} + +export const useLiveTitle = create((set) => ({ + titles: {}, + setTitle: (sessionId, title) => + set((state) => ({ titles: { ...state.titles, [sessionId]: title } })), + clearTitle: (sessionId) => + set((state) => { + const { [sessionId]: _, ...rest } = state.titles; + return { titles: rest }; + }), +})); + +export function useSessionTitle( + sessionId: string, + storeTitle: string | undefined, +): string { + const liveTitle = useLiveTitle((s) => s.titles[sessionId]); + return liveTitle ?? (storeTitle as string) ?? "Untitled"; +}