diff --git a/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx b/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx new file mode 100644 index 000000000..bf87c099d --- /dev/null +++ b/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState } from "react"; + +interface HeaderTitleEditorProps { + initialTitle: string; + onSubmit: (newTitle: string) => void; + onCancel: () => void; +} + +export function HeaderTitleEditor({ + initialTitle, + onSubmit, + onCancel, +}: HeaderTitleEditorProps) { + const [editValue, setEditValue] = useState(initialTitle); + const inputRef = useRef(null); + const resolvedRef = useRef(false); + + useEffect(() => { + const input = inputRef.current; + if (input) { + input.focus(); + input.setSelectionRange(input.value.length, input.value.length); + } + }, []); + + const handleSubmit = () => { + if (resolvedRef.current) return; + resolvedRef.current = true; + const trimmed = editValue.trim(); + if (trimmed && trimmed !== initialTitle) { + onSubmit(trimmed); + } else { + onCancel(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleSubmit(); + } else if (e.key === "Escape") { + e.preventDefault(); + resolvedRef.current = true; + onCancel(); + } + }; + + return ( + setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSubmit} + className="no-drag h-5 min-w-0 flex-1 rounded-sm border border-accent-8 bg-gray-2 px-1 font-medium text-[12px] text-gray-12 outline-none" + /> + ); +} diff --git a/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx b/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx index 5b233b4b4..464e563d3 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx @@ -8,8 +8,10 @@ import { getLeafPanel, parseTabId, } from "@features/panels/store/panelStoreHelpers"; +import { getSessionService } from "@features/sessions/service/service"; import { useCwd } from "@features/sidebar/hooks/useCwd"; import { useTaskData } from "@features/task-detail/hooks/useTaskData"; +import { useUpdateTask } from "@features/tasks/hooks/useTasks"; import { useTaskStore } from "@features/tasks/stores/taskStore"; import { useWorkspaceEvents } from "@features/workspace/hooks"; import { useWorkspace } from "@features/workspace/hooks/useWorkspace"; @@ -18,11 +20,16 @@ import { useFileWatcher } from "@hooks/useFileWatcher"; import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; import { Box, Flex, Text } from "@radix-ui/themes"; import type { Task } from "@shared/types"; +import { useQueryClient } from "@tanstack/react-query"; +import { logger } from "@utils/logger"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useHotkeys, useHotkeysContext } from "react-hotkeys-hook"; import { ExternalAppsOpener } from "./ExternalAppsOpener"; +import { HeaderTitleEditor } from "./HeaderTitleEditor"; + const MIN_REVIEW_WIDTH = 300; +const log = logger.scope("task-detail"); interface TaskDetailProps { task: Task; @@ -85,12 +92,63 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { useBlurOnEscape(); useWorkspaceEvents(taskId); + const [isEditingTitle, setIsEditingTitle] = useState(false); + const updateTask = useUpdateTask(); + const queryClient = useQueryClient(); + + const handleTitleEditSubmit = useCallback( + async (newTitle: string) => { + setIsEditingTitle(false); + + queryClient.setQueriesData( + { queryKey: ["tasks", "list"] }, + (old) => + old?.map((t) => + t.id === taskId + ? { ...t, title: newTitle, title_manually_set: true } + : t, + ), + ); + + getSessionService().updateSessionTaskTitle(taskId, newTitle); + + try { + await updateTask.mutateAsync({ + taskId, + updates: { title: newTitle, title_manually_set: true }, + }); + } catch (error) { + log.error("Failed to rename task", error); + getSessionService().updateSessionTaskTitle(taskId, task.title); + queryClient.invalidateQueries({ queryKey: ["tasks", "list"] }); + } + }, + [taskId, task.title, updateTask, queryClient], + ); + + const handleTitleEditCancel = useCallback(() => { + setIsEditingTitle(false); + }, []); const headerContent = useMemo( () => ( - - {task.title} - + {isEditingTitle ? ( + + ) : ( + setIsEditingTitle(true)} + > + {task.title} + + )} {openTargetPath && ( @@ -98,7 +156,13 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { )} ), - [task.title, openTargetPath], + [ + task.title, + openTargetPath, + isEditingTitle, + handleTitleEditSubmit, + handleTitleEditCancel, + ], ); useSetHeaderContent(headerContent);