Skip to content

Commit b2d8c81

Browse files
committed
feat: add setting to toggle task titles
1 parent 756ca5c commit b2d8c81

File tree

16 files changed

+206
-58
lines changed

16 files changed

+206
-58
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export const globalSettingsSchema = z.object({
148148
includeTaskHistoryInEnhance: z.boolean().optional(),
149149
historyPreviewCollapsed: z.boolean().optional(),
150150
reasoningBlockCollapsed: z.boolean().optional(),
151+
taskTitlesEnabled: z.boolean().optional(),
151152
profileThresholds: z.record(z.string(), z.number()).optional(),
152153
hasOpenedModeSelector: z.boolean().optional(),
153154
lastModeExportPath: z.string().optional(),

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,6 +1793,7 @@ export class ClineProvider
17931793
terminalCompressProgressBar,
17941794
historyPreviewCollapsed,
17951795
reasoningBlockCollapsed,
1796+
taskTitlesEnabled,
17961797
cloudUserInfo,
17971798
cloudIsAuthenticated,
17981799
sharingEnabled,
@@ -1866,6 +1867,7 @@ export class ClineProvider
18661867
taskHistory: (taskHistory || [])
18671868
.filter((item: HistoryItem) => item.ts && item.task)
18681869
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
1870+
taskTitlesEnabled: taskTitlesEnabled ?? false,
18691871
soundEnabled: soundEnabled ?? false,
18701872
ttsEnabled: ttsEnabled ?? false,
18711873
ttsSpeed: ttsSpeed ?? 1.0,
@@ -2142,6 +2144,7 @@ export class ClineProvider
21422144
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
21432145
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
21442146
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
2147+
taskTitlesEnabled: stateValues.taskTitlesEnabled ?? false,
21452148
cloudUserInfo,
21462149
cloudIsAuthenticated,
21472150
sharingEnabled,

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,10 @@ export const webviewMessageHandler = async (
16731673
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
16741674
// No need to call postStateToWebview here as the UI already updated optimistically
16751675
break
1676+
case "setTaskTitlesEnabled":
1677+
await updateGlobalState("taskTitlesEnabled", message.bool ?? false)
1678+
await provider.postStateToWebview()
1679+
break
16761680
case "toggleApiConfigPin":
16771681
if (message.text) {
16781682
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ export type ExtensionState = Pick<
288288
| "openRouterImageGenerationSelectedModel"
289289
| "includeTaskHistoryInEnhance"
290290
| "reasoningBlockCollapsed"
291+
| "taskTitlesEnabled"
291292
> & {
292293
version: string
293294
clineMessages: ClineMessage[]
@@ -327,6 +328,7 @@ export type ExtensionState = Pick<
327328
renderContext: "sidebar" | "editor"
328329
settingsImportedAt?: number
329330
historyPreviewCollapsed?: boolean
331+
taskTitlesEnabled?: boolean
330332

331333
cloudUserInfo: CloudUserInfo | null
332334
cloudIsAuthenticated: boolean

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export interface WebviewMessage {
196196
| "profileThresholds"
197197
| "setHistoryPreviewCollapsed"
198198
| "setReasoningBlockCollapsed"
199+
| "setTaskTitlesEnabled"
199200
| "openExternal"
200201
| "filterMarketplaceItems"
201202
| "marketplaceButtonClicked"

webview-ui/src/components/chat/TaskHeader.tsx

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const TaskHeader = ({
5353
todos,
5454
}: TaskHeaderProps) => {
5555
const { t } = useTranslation()
56-
const { apiConfiguration, currentTaskItem, clineMessages } = useExtensionState()
56+
const { apiConfiguration, currentTaskItem, clineMessages, taskTitlesEnabled = false } = useExtensionState()
5757
const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
5858
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
5959
const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false)
@@ -102,16 +102,25 @@ const TaskHeader = ({
102102
}, [currentTaskItem?.id])
103103

104104
useEffect(() => {
105+
if (!taskTitlesEnabled) {
106+
setIsEditingTitle(false)
107+
return
108+
}
109+
105110
if (isEditingTitle) {
106111
skipBlurSubmitRef.current = false
107112
requestAnimationFrame(() => {
108113
titleInputRef.current?.focus()
109114
titleInputRef.current?.select()
110115
})
111116
}
112-
}, [isEditingTitle])
117+
}, [isEditingTitle, taskTitlesEnabled])
113118

114119
const submitTitle = useCallback(() => {
120+
if (!taskTitlesEnabled) {
121+
return
122+
}
123+
115124
if (!currentTaskItem) {
116125
setIsEditingTitle(false)
117126
return
@@ -134,7 +143,7 @@ const TaskHeader = ({
134143
})
135144

136145
setTitleInput(trimmed)
137-
}, [currentTaskItem, titleInput])
146+
}, [currentTaskItem, taskTitlesEnabled, titleInput])
138147

139148
useEffect(() => {
140149
if (!isEditingTitle) {
@@ -143,15 +152,22 @@ const TaskHeader = ({
143152
}, [isEditingTitle])
144153

145154
const handleTitleBlur = useCallback(() => {
155+
if (!taskTitlesEnabled) {
156+
return
157+
}
146158
if (skipBlurSubmitRef.current) {
147159
skipBlurSubmitRef.current = false
148160
return
149161
}
150162
submitTitle()
151-
}, [submitTitle])
163+
}, [submitTitle, taskTitlesEnabled])
152164

153165
const handleTitleKeyDown = useCallback(
154166
(event: ReactKeyboardEvent<HTMLInputElement>) => {
167+
if (!taskTitlesEnabled) {
168+
return
169+
}
170+
155171
if (event.key === "Enter") {
156172
event.preventDefault()
157173
skipBlurSubmitRef.current = true
@@ -163,7 +179,7 @@ const TaskHeader = ({
163179
setTitleInput(currentTaskItem?.title ?? "")
164180
}
165181
},
166-
[currentTaskItem?.title, submitTitle],
182+
[currentTaskItem?.title, submitTitle, taskTitlesEnabled],
167183
)
168184

169185
const textContainerRef = useRef<HTMLDivElement>(null)
@@ -181,9 +197,13 @@ const TaskHeader = ({
181197
</StandardTooltip>
182198
)
183199

184-
const renderTitleSection = () => {
185-
if (!currentTaskItem) {
186-
return null
200+
const renderPrimaryValue = () => {
201+
if (!taskTitlesEnabled || !currentTaskItem) {
202+
return (
203+
<div className="truncate min-w-0" data-testid="task-primary-text">
204+
<Mention text={task.text} />
205+
</div>
206+
)
187207
}
188208

189209
if (isEditingTitle) {
@@ -203,17 +223,20 @@ const TaskHeader = ({
203223
}
204224

205225
const tooltipKey = currentTitle.length > 0 ? "chat:task.editTitle" : "chat:task.addTitle"
226+
const showTitle = currentTitle.length > 0
227+
const displayNode = showTitle ? (
228+
<span className="truncate text-base font-semibold" data-testid="task-title-text">
229+
{currentTitle}
230+
</span>
231+
) : (
232+
<div className="truncate min-w-0" data-testid="task-title-text">
233+
<Mention text={task.text} />
234+
</div>
235+
)
206236

207237
return (
208-
<div className="flex items-center gap-1 text-vscode-foreground" data-testid="task-title-display">
209-
<span
210-
className={cn(
211-
"text-base font-semibold truncate max-w-full",
212-
currentTitle.length === 0 && "italic text-vscode-descriptionForeground font-normal",
213-
)}
214-
data-testid="task-title-text">
215-
{currentTitle || t("chat:task.titlePlaceholder")}
216-
</span>
238+
<div className="flex items-center gap-1 min-w-0 text-vscode-foreground" data-testid="task-title-display">
239+
<div className="min-w-0 flex-1">{displayNode}</div>
217240
<StandardTooltip content={t(tooltipKey)}>
218241
<button
219242
type="button"
@@ -233,6 +256,13 @@ const TaskHeader = ({
233256
)
234257
}
235258

259+
const renderCollapsedSummary = () => (
260+
<div className="flex items-baseline gap-1 min-w-0">
261+
<span className="font-bold shrink-0">{t("chat:task.title")}</span>
262+
<div className="min-w-0 flex-1">{renderPrimaryValue()}</div>
263+
</div>
264+
)
265+
236266
const hasTodos = todos && Array.isArray(todos) && todos.length > 0
237267

238268
return (
@@ -279,15 +309,14 @@ const TaskHeader = ({
279309
}}>
280310
<div className="flex justify-between items-start gap-0">
281311
<div className="flex flex-col grow min-w-0 gap-1">
282-
{renderTitleSection()}
283312
<div className="whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0 select-none">
284313
{isTaskExpanded ? (
285-
<span className="font-bold">{t("chat:task.title")}</span>
286-
) : (
287-
<div className="flex items-baseline gap-1">
314+
<div className="flex flex-col gap-1 min-w-0">
288315
<span className="font-bold">{t("chat:task.title")}</span>
289-
<Mention text={task.text} />
316+
<div className="min-w-0">{renderPrimaryValue()}</div>
290317
</div>
318+
) : (
319+
renderCollapsedSummary()
291320
)}
292321
</div>
293322
</div>

webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ let mockExtensionState: {
4949
apiConfiguration: ProviderSettings
5050
currentTaskItem: { id: string } | null
5151
clineMessages: any[]
52+
taskTitlesEnabled: boolean
5253
} = {
5354
apiConfiguration: {
5455
apiProvider: "anthropic",
@@ -57,6 +58,7 @@ let mockExtensionState: {
5758
} as ProviderSettings,
5859
currentTaskItem: { id: "test-task-id" },
5960
clineMessages: [],
61+
taskTitlesEnabled: true,
6062
}
6163

6264
// Mock the ExtensionStateContext
@@ -111,6 +113,7 @@ describe("TaskHeader", () => {
111113
} as ProviderSettings,
112114
currentTaskItem: { id: "test-task-id" },
113115
clineMessages: [],
116+
taskTitlesEnabled: true,
114117
}
115118
})
116119
const defaultProps: TaskHeaderProps = {
@@ -221,6 +224,14 @@ describe("TaskHeader", () => {
221224
})
222225
})
223226

227+
it("hides title controls when task titles are disabled", () => {
228+
mockExtensionState.taskTitlesEnabled = false
229+
renderTaskHeader()
230+
231+
expect(screen.queryByTestId("task-title-edit-button")).not.toBeInTheDocument()
232+
expect(screen.queryByTestId("task-title-display")).not.toBeInTheDocument()
233+
})
234+
224235
describe("DismissibleUpsell behavior", () => {
225236
beforeEach(() => {
226237
vi.useFakeTimers()
@@ -233,6 +244,7 @@ describe("TaskHeader", () => {
233244
} as ProviderSettings,
234245
currentTaskItem: { id: "test-task-id" },
235246
clineMessages: [],
247+
taskTitlesEnabled: true,
236248
}
237249
})
238250

webview-ui/src/components/history/TaskItem.tsx

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { HistoryItem } from "@roo-code/types"
44
import { vscode } from "@/utils/vscode"
55
import { cn } from "@/lib/utils"
66
import { Checkbox } from "@/components/ui/checkbox"
7+
import { useExtensionState } from "@/context/ExtensionStateContext"
78

89
import TaskItemFooter from "./TaskItemFooter"
910

@@ -33,6 +34,7 @@ const TaskItem = ({
3334
onDelete,
3435
className,
3536
}: TaskItemProps) => {
37+
const { taskTitlesEnabled = false } = useExtensionState()
3638
const handleClick = () => {
3739
if (isSelectionMode && onToggleSelection) {
3840
onToggleSelection(item.id, !isSelected)
@@ -42,6 +44,9 @@ const TaskItem = ({
4244
}
4345

4446
const isCompact = variant === "compact"
47+
const showTitle = taskTitlesEnabled && Boolean(item.title?.trim())
48+
const displayHighlight = showTitle && item.titleHighlight ? item.titleHighlight : item.highlight
49+
const displayText = showTitle && item.title ? item.title : item.task
4550

4651
return (
4752
<div
@@ -69,37 +74,24 @@ const TaskItem = ({
6974
)}
7075

7176
<div className="flex-1 min-w-0">
72-
{(item.title || item.titleHighlight) &&
73-
(item.titleHighlight ? (
74-
<div
75-
className={cn("text-vscode-foreground font-semibold truncate mb-1", {
76-
"text-base": !isCompact,
77-
"text-sm": isCompact,
78-
})}
79-
data-testid="task-item-title"
80-
dangerouslySetInnerHTML={{ __html: item.titleHighlight }}
81-
/>
82-
) : (
83-
<div
84-
className={cn("text-vscode-foreground font-semibold truncate mb-1", {
85-
"text-base": !isCompact,
86-
"text-sm": isCompact,
87-
})}
88-
data-testid="task-item-title">
89-
{item.title}
90-
</div>
91-
))}
9277
<div
9378
className={cn(
9479
"overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis line-clamp-2",
9580
{
9681
"text-base": !isCompact,
82+
"text-sm": isCompact,
9783
},
9884
!isCompact && isSelectionMode ? "mb-1" : "",
9985
)}
100-
data-testid="task-content"
101-
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
102-
{item.highlight ? undefined : item.task}
86+
data-testid="task-content">
87+
{displayHighlight ? (
88+
<span
89+
className={cn(showTitle && "font-semibold")}
90+
dangerouslySetInnerHTML={{ __html: displayHighlight }}
91+
/>
92+
) : (
93+
<span className={cn(showTitle && "font-semibold")}>{displayText}</span>
94+
)}
10395
</div>
10496
<TaskItemFooter
10597
item={item}

0 commit comments

Comments
 (0)