From 689f9fbc5c4c1a1c0b499d60b3e7e3d691eaff9d Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Mon, 23 Feb 2026 00:28:03 +0900 Subject: [PATCH 1/3] refactor: remove debounced persist from title input Remove custom debouncing implementation with refs and timeouts. Replace with immediate store updates on blur event using localTitle state. Simplifies title persistence logic by removing debouncedPersist and flushDebounce functions. --- .../main/body/sessions/title-input.tsx | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) 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..b7e69ae345 100644 --- a/apps/desktop/src/components/main/body/sessions/title-input.tsx +++ b/apps/desktop/src/components/main/body/sessions/title-input.tsx @@ -149,36 +149,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) => { @@ -238,7 +208,6 @@ const TitleInputInner = memo( const beforeCursor = input.value.slice(0, cursorPos); const afterCursor = input.value.slice(cursorPos); - flushDebounce(); setStoreTitle(beforeCursor); if (afterCursor) { @@ -299,7 +268,6 @@ const TitleInputInner = memo( type="text" onChange={(e) => { setLocalTitle(e.target.value); - debouncedPersist(e.target.value); }} onKeyDown={handleKeyDown} onFocus={() => { @@ -307,7 +275,7 @@ const TitleInputInner = memo( }} onBlur={() => { isFocused.current = false; - flushDebounce(); + setStoreTitle(localTitle); }} value={localTitle} className={cn([ From 74e822d4c5a26d2d1a31f90e02581b761a9405e1 Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Mon, 23 Feb 2026 01:26:36 +0900 Subject: [PATCH 2/3] feat: Add live title updates for active sessions Implement real-time title synchronization using Zustand store to show immediate title changes across the UI while user types. Create useLiveTitle store with setTitle/clearTitle actions and useSessionTitle hook for consistent title resolution. Integrate live title updates in TitleInput onChange and clear on blur/enter. Update timeline items and folder components to display live titles. Minor UI improvement to toast button styling with shadow and color. --- .../components/main/body/sessions/index.tsx | 9 ++++++- .../body/sessions/outer-header/folder.tsx | 11 +++++--- .../main/body/sessions/title-input.tsx | 10 +++++-- .../components/main/sidebar/timeline/item.tsx | 12 ++++++--- apps/desktop/src/store/zustand/live-title.ts | 26 +++++++++++++++++++ 5 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/src/store/zustand/live-title.ts 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 b7e69ae345..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,7 +152,6 @@ const TitleInputInner = memo( main.STORE_ID, ); - useEffect(() => { const handleMoveToTitlePosition = (e: Event) => { const customEvent = e as CustomEvent<{ pixelWidth: number }>; @@ -209,6 +211,7 @@ const TitleInputInner = memo( const afterCursor = input.value.slice(cursorPos); setStoreTitle(beforeCursor); + clearLiveTitle(sessionId); if (afterCursor) { setTimeout(() => { @@ -267,7 +270,9 @@ const TitleInputInner = memo( placeholder="Untitled" type="text" onChange={(e) => { - setLocalTitle(e.target.value); + const value = e.target.value; + setLocalTitle(value); + setLiveTitle(sessionId, value); }} onKeyDown={handleKeyDown} onFocus={() => { @@ -276,6 +281,7 @@ const TitleInputInner = memo( onBlur={() => { isFocused.current = false; 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..fef0ca18f8 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"; @@ -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"; +} From e21fe74ec7886553bf15271947754265b0a35d45 Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Mon, 23 Feb 2026 01:33:56 +0900 Subject: [PATCH 3/3] feat: Add fallback for untitled timeline items Display "Untitled" placeholder when timeline item has no title to improve user experience and prevent empty list items from appearing in the sidebar. --- apps/desktop/src/components/main/sidebar/timeline/item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/components/main/sidebar/timeline/item.tsx b/apps/desktop/src/components/main/sidebar/timeline/item.tsx index fef0ca18f8..121da8814f 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/item.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/item.tsx @@ -128,7 +128,7 @@ function ItemBase({ ignored && "line-through", )} > - {title} + {title || Untitled} {displayTime && (
{displayTime}