diff --git a/src/renderer/src/hooks/useInPlaceEdit.ts b/src/renderer/src/hooks/useInPlaceEdit.ts index 537d89fc12..d912abd57e 100644 --- a/src/renderer/src/hooks/useInPlaceEdit.ts +++ b/src/renderer/src/hooks/useInPlaceEdit.ts @@ -1,7 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react' +import { useTimer } from './useTimer' + export interface UseInPlaceEditOptions { - onSave: (value: string) => void + onSave: ((value: string) => void) | ((value: string) => Promise) onCancel?: () => void autoSelectOnStart?: boolean trimOnSave?: boolean @@ -9,6 +11,7 @@ export interface UseInPlaceEditOptions { export interface UseInPlaceEditReturn { isEditing: boolean + isSaving: boolean editValue: string inputRef: React.RefObject startEdit: (initialValue: string) => void @@ -16,23 +19,27 @@ export interface UseInPlaceEditReturn { cancelEdit: () => void handleKeyDown: (e: React.KeyboardEvent) => void handleInputChange: (e: React.ChangeEvent) => void + handleValueChange: (value: string) => void } +/** + * A React hook that provides in-place editing functionality for text inputs + * @param options - Configuration options for the in-place edit behavior + * @param options.onSave - Callback function called when edits are saved + * @param options.onCancel - Optional callback function called when editing is cancelled + * @param options.autoSelectOnStart - Whether to automatically select text when editing starts (default: true) + * @param options.trimOnSave - Whether to trim whitespace when saving (default: true) + * @returns An object containing the editing state and handler functions + */ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditReturn { const { onSave, onCancel, autoSelectOnStart = true, trimOnSave = true } = options + const [isSaving, setIsSaving] = useState(false) const [isEditing, setIsEditing] = useState(false) const [editValue, setEditValue] = useState('') const [originalValue, setOriginalValue] = useState('') const inputRef = useRef(null) - - const editTimerRef = useRef(undefined) - - useEffect(() => { - return () => { - clearTimeout(editTimerRef.current) - } - }, []) + const { setTimeoutTimer } = useTimer() const startEdit = useCallback( (initialValue: string) => { @@ -40,26 +47,37 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe setEditValue(initialValue) setOriginalValue(initialValue) - clearTimeout(editTimerRef.current) - editTimerRef.current = setTimeout(() => { - inputRef.current?.focus() - if (autoSelectOnStart) { - inputRef.current?.select() - } - }, 0) + setTimeoutTimer( + 'startEdit', + () => { + inputRef.current?.focus() + if (autoSelectOnStart) { + inputRef.current?.select() + } + }, + 0 + ) }, - [autoSelectOnStart] + [autoSelectOnStart, setTimeoutTimer] ) - const saveEdit = useCallback(() => { - const finalValue = trimOnSave ? editValue.trim() : editValue - if (finalValue !== originalValue) { - onSave(finalValue) + const saveEdit = useCallback(async () => { + if (isSaving) return + + setIsSaving(true) + + try { + const finalValue = trimOnSave ? editValue.trim() : editValue + if (finalValue !== originalValue) { + await onSave(finalValue) + } + setIsEditing(false) + setEditValue('') + setOriginalValue('') + } finally { + setIsSaving(false) } - setIsEditing(false) - setEditValue('') - setOriginalValue('') - }, [editValue, originalValue, onSave, trimOnSave]) + }, [isSaving, trimOnSave, editValue, originalValue, onSave]) const cancelEdit = useCallback(() => { setIsEditing(false) @@ -86,6 +104,10 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe setEditValue(e.target.value) }, []) + const handleValueChange = useCallback((value: string) => { + setEditValue(value) + }, []) + // Handle clicks outside the input to save useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -105,12 +127,14 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe return { isEditing, + isSaving, editValue, inputRef, startEdit, saveEdit, cancelEdit, handleKeyDown, - handleInputChange + handleInputChange, + handleValueChange } } diff --git a/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx b/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx index e51d485dd4..f932430190 100644 --- a/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/SessionItem.tsx @@ -1,6 +1,8 @@ -import { Button, cn, useDisclosure } from '@heroui/react' +import { Button, cn, Input, useDisclosure } from '@heroui/react' import { DeleteIcon, EditIcon } from '@renderer/components/Icons' import { SessionModal } from '@renderer/components/Popups/agent/SessionModal' +import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' +import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' import { useRuntime } from '@renderer/hooks/useRuntime' import { AgentSessionEntity } from '@renderer/types' import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu' @@ -23,15 +25,24 @@ const SessionItem: FC = ({ session, agentId, isDisabled, isLoa const { t } = useTranslation() const { isOpen, onOpen, onClose } = useDisclosure() const { chat } = useRuntime() + const updateSession = useUpdateSession(agentId) const activeSessionId = chat.activeSessionId[agentId] + const { isEditing, isSaving, editValue, inputRef, startEdit, handleKeyDown, handleValueChange } = useInPlaceEdit({ + onSave: async (value) => { + if (value !== session.name) { + await updateSession({ id: session.id, name: value }) + } + } + }) + const isActive = activeSessionId === session.id const SessionLabel = useCallback(() => { const displayName = session.name ?? session.id return ( <> - {displayName} + {displayName} ) }, [session.id, session.name]) @@ -44,9 +55,26 @@ const SessionItem: FC = ({ session, agentId, isDisabled, isLoa isDisabled={isDisabled} isLoading={isLoading} onPress={onPress} - className={isActive ? 'active' : ''}> - - + className={isActive ? 'active' : ''} + onDoubleClick={() => startEdit(session.name ?? '')}> + + {isEditing && ( + e.stopPropagation()} + classNames={{ + base: 'h-full', + mainWrapper: 'h-full', + inputWrapper: 'h-full min-h-0 px-1.5', + input: isSaving ? 'brightness-50' : undefined + }} + /> + )} + {!isEditing && } @@ -79,7 +107,7 @@ const ButtonContainer: React.FC> = ({ classN