From 45472f9608922e29bee1d5c57c9987f17b07b7b1 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 7 Apr 2026 19:15:48 -0700 Subject: [PATCH 1/3] Add double-click to rename task title in session header --- .../components/HeaderTitleEditor.tsx | 56 +++++++++++++++ .../task-detail/components/TaskDetail.tsx | 72 +++++++++++++++++-- 2 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx 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..d11443111 --- /dev/null +++ b/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx @@ -0,0 +1,56 @@ +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); + + useEffect(() => { + const input = inputRef.current; + if (input) { + input.focus(); + input.setSelectionRange(input.value.length, input.value.length); + } + }, []); + + const handleSubmit = () => { + 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(); + onCancel(); + } + }; + + return ( + setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSubmit} + className="no-drag 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" + style={{ height: "20px" }} + /> + ); +} 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..a3d629417 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,10 +20,14 @@ 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; interface TaskDetailProps { @@ -85,12 +91,64 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { useBlurOnEscape(); useWorkspaceEvents(taskId); + const [isEditingTitle, setIsEditingTitle] = useState(false); + const updateTask = useUpdateTask(); + const queryClient = useQueryClient(); + const log = useMemo(() => logger.scope("task-detail"), []); + + 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); + queryClient.invalidateQueries({ queryKey: ["tasks", "list"] }); + } + }, + [taskId, updateTask, queryClient, log], + ); + + 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); From 7922e1a8f0f70f0511c01f54c6a558936fe94e68 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 7 Apr 2026 20:52:35 -0700 Subject: [PATCH 2/3] Fix blur/escape race and rollback in title editor --- .../features/task-detail/components/HeaderTitleEditor.tsx | 7 +++++-- .../features/task-detail/components/TaskDetail.tsx | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx b/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx index d11443111..bf87c099d 100644 --- a/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx +++ b/apps/code/src/renderer/features/task-detail/components/HeaderTitleEditor.tsx @@ -13,6 +13,7 @@ export function HeaderTitleEditor({ }: HeaderTitleEditorProps) { const [editValue, setEditValue] = useState(initialTitle); const inputRef = useRef(null); + const resolvedRef = useRef(false); useEffect(() => { const input = inputRef.current; @@ -23,6 +24,8 @@ export function HeaderTitleEditor({ }, []); const handleSubmit = () => { + if (resolvedRef.current) return; + resolvedRef.current = true; const trimmed = editValue.trim(); if (trimmed && trimmed !== initialTitle) { onSubmit(trimmed); @@ -37,6 +40,7 @@ export function HeaderTitleEditor({ handleSubmit(); } else if (e.key === "Escape") { e.preventDefault(); + resolvedRef.current = true; onCancel(); } }; @@ -49,8 +53,7 @@ export function HeaderTitleEditor({ onChange={(e) => setEditValue(e.target.value)} onKeyDown={handleKeyDown} onBlur={handleSubmit} - className="no-drag 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" - style={{ height: "20px" }} + 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 a3d629417..dfcae5a43 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx @@ -29,6 +29,7 @@ import { ExternalAppsOpener } from "./ExternalAppsOpener"; import { HeaderTitleEditor } from "./HeaderTitleEditor"; const MIN_REVIEW_WIDTH = 300; +const log = logger.scope("task-detail"); interface TaskDetailProps { task: Task; @@ -94,7 +95,6 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { const [isEditingTitle, setIsEditingTitle] = useState(false); const updateTask = useUpdateTask(); const queryClient = useQueryClient(); - const log = useMemo(() => logger.scope("task-detail"), []); const handleTitleEditSubmit = useCallback( async (newTitle: string) => { @@ -119,10 +119,11 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { }); } catch (error) { log.error("Failed to rename task", error); + getSessionService().updateSessionTaskTitle(taskId, task.title); queryClient.invalidateQueries({ queryKey: ["tasks", "list"] }); } }, - [taskId, updateTask, queryClient, log], + [taskId, task.title, updateTask, queryClient], ); const handleTitleEditCancel = useCallback(() => { From 78d79c7ed6e568438e5cf30799012f7f5db0d1ce Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 7 Apr 2026 20:54:50 -0700 Subject: [PATCH 3/3] Replace inline minWidth style with Tailwind class --- .../renderer/features/task-detail/components/TaskDetail.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 dfcae5a43..464e563d3 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx @@ -143,8 +143,7 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) { size="1" weight="medium" truncate - className="no-drag" - style={{ minWidth: 0 }} + className="no-drag min-w-0" onDoubleClick={() => setIsEditingTitle(true)} > {task.title}