Skip to content

Commit aee3c91

Browse files
authored
feat: Add double-click to rename task title in session header (#1544)
## Problem Task titles in the session view header are read-only, requiring users to go to the sidebar to rename. This makes renaming less discoverable as shown by every time I tell someone you can rename the task by double clicking the title they try this title and fail, not the one in the sidebar. ## Changes 1. Add HeaderTitleEditor inline edit component with auto-focus and keyboard handling 2. Wire double-click on header title to toggle into edit mode 3. Persist rename with optimistic cache update, session store sync and API call (same pattern as sidebar) ## How did you test this? Manually
1 parent 26f0373 commit aee3c91

2 files changed

Lines changed: 127 additions & 4 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useEffect, useRef, useState } from "react";
2+
3+
interface HeaderTitleEditorProps {
4+
initialTitle: string;
5+
onSubmit: (newTitle: string) => void;
6+
onCancel: () => void;
7+
}
8+
9+
export function HeaderTitleEditor({
10+
initialTitle,
11+
onSubmit,
12+
onCancel,
13+
}: HeaderTitleEditorProps) {
14+
const [editValue, setEditValue] = useState(initialTitle);
15+
const inputRef = useRef<HTMLInputElement>(null);
16+
const resolvedRef = useRef(false);
17+
18+
useEffect(() => {
19+
const input = inputRef.current;
20+
if (input) {
21+
input.focus();
22+
input.setSelectionRange(input.value.length, input.value.length);
23+
}
24+
}, []);
25+
26+
const handleSubmit = () => {
27+
if (resolvedRef.current) return;
28+
resolvedRef.current = true;
29+
const trimmed = editValue.trim();
30+
if (trimmed && trimmed !== initialTitle) {
31+
onSubmit(trimmed);
32+
} else {
33+
onCancel();
34+
}
35+
};
36+
37+
const handleKeyDown = (e: React.KeyboardEvent) => {
38+
if (e.key === "Enter") {
39+
e.preventDefault();
40+
handleSubmit();
41+
} else if (e.key === "Escape") {
42+
e.preventDefault();
43+
resolvedRef.current = true;
44+
onCancel();
45+
}
46+
};
47+
48+
return (
49+
<input
50+
ref={inputRef}
51+
type="text"
52+
value={editValue}
53+
onChange={(e) => setEditValue(e.target.value)}
54+
onKeyDown={handleKeyDown}
55+
onBlur={handleSubmit}
56+
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"
57+
/>
58+
);
59+
}

apps/code/src/renderer/features/task-detail/components/TaskDetail.tsx

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
getLeafPanel,
99
parseTabId,
1010
} from "@features/panels/store/panelStoreHelpers";
11+
import { getSessionService } from "@features/sessions/service/service";
1112
import { useCwd } from "@features/sidebar/hooks/useCwd";
1213
import { useTaskData } from "@features/task-detail/hooks/useTaskData";
14+
import { useUpdateTask } from "@features/tasks/hooks/useTasks";
1315
import { useTaskStore } from "@features/tasks/stores/taskStore";
1416
import { useWorkspaceEvents } from "@features/workspace/hooks";
1517
import { useWorkspace } from "@features/workspace/hooks/useWorkspace";
@@ -18,11 +20,16 @@ import { useFileWatcher } from "@hooks/useFileWatcher";
1820
import { useSetHeaderContent } from "@hooks/useSetHeaderContent";
1921
import { Box, Flex, Text } from "@radix-ui/themes";
2022
import type { Task } from "@shared/types";
23+
import { useQueryClient } from "@tanstack/react-query";
24+
import { logger } from "@utils/logger";
2125
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2226
import { useHotkeys, useHotkeysContext } from "react-hotkeys-hook";
2327
import { ExternalAppsOpener } from "./ExternalAppsOpener";
2428

29+
import { HeaderTitleEditor } from "./HeaderTitleEditor";
30+
2531
const MIN_REVIEW_WIDTH = 300;
32+
const log = logger.scope("task-detail");
2633

2734
interface TaskDetailProps {
2835
task: Task;
@@ -85,20 +92,77 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) {
8592
useBlurOnEscape();
8693
useWorkspaceEvents(taskId);
8794

95+
const [isEditingTitle, setIsEditingTitle] = useState(false);
96+
const updateTask = useUpdateTask();
97+
const queryClient = useQueryClient();
98+
99+
const handleTitleEditSubmit = useCallback(
100+
async (newTitle: string) => {
101+
setIsEditingTitle(false);
102+
103+
queryClient.setQueriesData<Task[]>(
104+
{ queryKey: ["tasks", "list"] },
105+
(old) =>
106+
old?.map((t) =>
107+
t.id === taskId
108+
? { ...t, title: newTitle, title_manually_set: true }
109+
: t,
110+
),
111+
);
112+
113+
getSessionService().updateSessionTaskTitle(taskId, newTitle);
114+
115+
try {
116+
await updateTask.mutateAsync({
117+
taskId,
118+
updates: { title: newTitle, title_manually_set: true },
119+
});
120+
} catch (error) {
121+
log.error("Failed to rename task", error);
122+
getSessionService().updateSessionTaskTitle(taskId, task.title);
123+
queryClient.invalidateQueries({ queryKey: ["tasks", "list"] });
124+
}
125+
},
126+
[taskId, task.title, updateTask, queryClient],
127+
);
128+
129+
const handleTitleEditCancel = useCallback(() => {
130+
setIsEditingTitle(false);
131+
}, []);
88132
const headerContent = useMemo(
89133
() => (
90134
<Flex align="center" justify="between" gap="2" width="100%">
91-
<Text size="1" weight="medium" truncate style={{ minWidth: 0 }}>
92-
{task.title}
93-
</Text>
135+
{isEditingTitle ? (
136+
<HeaderTitleEditor
137+
initialTitle={task.title}
138+
onSubmit={handleTitleEditSubmit}
139+
onCancel={handleTitleEditCancel}
140+
/>
141+
) : (
142+
<Text
143+
size="1"
144+
weight="medium"
145+
truncate
146+
className="no-drag min-w-0"
147+
onDoubleClick={() => setIsEditingTitle(true)}
148+
>
149+
{task.title}
150+
</Text>
151+
)}
94152
{openTargetPath && (
95153
<Flex align="center" gap="2" className="shrink-0">
96154
<ExternalAppsOpener targetPath={openTargetPath} />
97155
</Flex>
98156
)}
99157
</Flex>
100158
),
101-
[task.title, openTargetPath],
159+
[
160+
task.title,
161+
openTargetPath,
162+
isEditingTitle,
163+
handleTitleEditSubmit,
164+
handleTitleEditCancel,
165+
],
102166
);
103167

104168
useSetHeaderContent(headerContent);

0 commit comments

Comments
 (0)