diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts
index 9a17834ced..a56a125d08 100644
--- a/packages/types/src/global-settings.ts
+++ b/packages/types/src/global-settings.ts
@@ -193,6 +193,7 @@ export const globalSettingsSchema = z.object({
* @default "send"
*/
enterBehavior: z.enum(["send", "newline"]).optional(),
+ taskTitlesEnabled: z.boolean().optional(),
profileThresholds: z.record(z.string(), z.number()).optional(),
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),
diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts
index a06ccc45d1..44cdd60b0a 100644
--- a/packages/types/src/vscode-extension-host.ts
+++ b/packages/types/src/vscode-extension-host.ts
@@ -262,6 +262,7 @@ export type ExtensionState = Pick<
| "includeCurrentCost"
| "maxGitStatusFiles"
| "requestDelaySeconds"
+ | "taskTitlesEnabled"
> & {
version: string
clineMessages: ClineMessage[]
@@ -302,6 +303,7 @@ export type ExtensionState = Pick<
renderContext: "sidebar" | "editor"
settingsImportedAt?: number
historyPreviewCollapsed?: boolean
+ taskTitlesEnabled?: boolean
cloudUserInfo: CloudUserInfo | null
cloudIsAuthenticated: boolean
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 33dad9226f..e80c0afbde 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -2003,6 +2003,7 @@ export class ClineProvider
historyPreviewCollapsed,
reasoningBlockCollapsed,
enterBehavior,
+ taskTitlesEnabled,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
@@ -2093,6 +2094,7 @@ export class ClineProvider
taskHistory: (taskHistory || [])
.filter((item: HistoryItem) => item.ts && item.task)
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
+ taskTitlesEnabled: taskTitlesEnabled ?? false,
soundEnabled: soundEnabled ?? false,
ttsEnabled: ttsEnabled ?? false,
ttsSpeed: ttsSpeed ?? 1.0,
@@ -2409,6 +2411,7 @@ export class ClineProvider
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
enterBehavior: stateValues.enterBehavior ?? "send",
+ taskTitlesEnabled: stateValues.taskTitlesEnabled ?? false,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts
index 4a049079e9..eb84f43bec 100644
--- a/src/core/webview/webviewMessageHandler.ts
+++ b/src/core/webview/webviewMessageHandler.ts
@@ -1668,7 +1668,6 @@ export const webviewMessageHandler = async (
await updateGlobalState("hasOpenedModeSelector", message.bool ?? true)
await provider.postStateToWebview()
break
-
case "toggleApiConfigPin":
if (message.text) {
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index f5da8e947d..fe8809828f 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -376,7 +376,9 @@ const TaskHeader = ({
{t("chat:task.title")}
{renderTitleAction()}
- {expandedTitleContent ?
{expandedTitleContent}
: null}
+ {expandedTitleContent ? (
+ {expandedTitleContent}
+ ) : null}
) : (
diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
index 7ee574604d..43f4b30dfb 100644
--- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
+++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
@@ -49,6 +49,7 @@ let mockExtensionState: {
apiConfiguration: ProviderSettings
currentTaskItem: { id: string } | null
clineMessages: any[]
+ taskTitlesEnabled: boolean
} = {
apiConfiguration: {
apiProvider: "anthropic",
@@ -57,6 +58,7 @@ let mockExtensionState: {
} as ProviderSettings,
currentTaskItem: { id: "test-task-id" },
clineMessages: [],
+ taskTitlesEnabled: true,
}
// Mock the ExtensionStateContext
@@ -111,6 +113,7 @@ describe("TaskHeader", () => {
} as ProviderSettings,
currentTaskItem: { id: "test-task-id" },
clineMessages: [],
+ taskTitlesEnabled: true,
}
})
const defaultProps: TaskHeaderProps = {
@@ -221,6 +224,14 @@ describe("TaskHeader", () => {
})
})
+ it("hides title controls when task titles are disabled", () => {
+ mockExtensionState.taskTitlesEnabled = false
+ renderTaskHeader()
+
+ expect(screen.queryByTestId("task-title-edit-button")).not.toBeInTheDocument()
+ expect(screen.queryByTestId("task-title-display")).not.toBeInTheDocument()
+ })
+
describe("DismissibleUpsell behavior", () => {
beforeEach(() => {
vi.useFakeTimers()
@@ -233,6 +244,7 @@ describe("TaskHeader", () => {
} as ProviderSettings,
currentTaskItem: { id: "test-task-id" },
clineMessages: [],
+ taskTitlesEnabled: true,
}
})
diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx
index 67e512e7e5..832650a68d 100644
--- a/webview-ui/src/components/history/TaskItem.tsx
+++ b/webview-ui/src/components/history/TaskItem.tsx
@@ -4,6 +4,7 @@ import type { HistoryItem } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { cn } from "@/lib/utils"
import { Checkbox } from "@/components/ui/checkbox"
+import { useExtensionState } from "@/context/ExtensionStateContext"
import TaskItemFooter from "./TaskItemFooter"
@@ -33,6 +34,7 @@ const TaskItem = ({
onDelete,
className,
}: TaskItemProps) => {
+ const { taskTitlesEnabled = false } = useExtensionState()
const handleClick = () => {
if (isSelectionMode && onToggleSelection) {
onToggleSelection(item.id, !isSelected)
@@ -42,6 +44,9 @@ const TaskItem = ({
}
const isCompact = variant === "compact"
+ const showTitle = taskTitlesEnabled && Boolean(item.title?.trim())
+ const displayHighlight = showTitle && item.titleHighlight ? item.titleHighlight : item.highlight
+ const displayText = showTitle && item.title ? item.title : item.task
return (
- {(item.title || item.titleHighlight) &&
- (item.titleHighlight ? (
-
- ) : (
-
- {item.title}
-
- ))}
- {item.highlight ? undefined : item.task}
+ data-testid="task-content">
+ {displayHighlight ? (
+
+ ) : (
+ {displayText}
+ )}
vi.fn(() => ({ taskTitlesEnabled: true })))
+vi.mock("@/context/ExtensionStateContext", () => ({
+ useExtensionState: mockUseExtensionState,
+}))
vi.mock("@src/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => key,
@@ -29,6 +33,7 @@ const mockTask = {
describe("TaskItem", () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockUseExtensionState.mockReturnValue({ taskTitlesEnabled: true })
})
it("renders task information", () => {
@@ -80,7 +85,7 @@ describe("TaskItem", () => {
expect(screen.getByTestId("export")).toBeInTheDocument()
})
- it("renders title above task content when provided", () => {
+ it("renders title instead of task text when provided", () => {
render(
{
/>,
)
- expect(screen.getByTestId("task-item-title")).toHaveTextContent("Important task")
- expect(screen.getByTestId("task-content")).toHaveTextContent("Test task")
+ const content = screen.getByTestId("task-content")
+ expect(content).toHaveTextContent("Important task")
+ expect(content).not.toHaveTextContent("Test task")
+ expect(content.querySelector("span")?.className).toContain("font-semibold")
+ })
+
+ it("falls back to task text when feature disabled", () => {
+ mockUseExtensionState.mockReturnValue({ taskTitlesEnabled: false })
+ render(
+ ,
+ )
+
+ const content = screen.getByTestId("task-content")
+ expect(content).toHaveTextContent("Test task")
+ expect(content).not.toHaveTextContent("Hidden title")
+ expect(content.querySelector("span")?.className || "").not.toContain("font-semibold")
})
it("displays time ago information", () => {
diff --git a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx
index 16b4b7d414..f2c9161470 100644
--- a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx
+++ b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx
@@ -59,6 +59,7 @@ describe("useTaskSearch", () => {
mockUseExtensionState.mockReturnValue({
taskHistory: mockTaskHistory,
cwd: "/workspace/project1",
+ taskTitlesEnabled: true,
} as any)
})
@@ -169,6 +170,23 @@ describe("useTaskSearch", () => {
expect((result.current.tasks[0] as any).titleHighlight).toBe("Build component")
})
+ it("ignores task titles in search when the feature is disabled", () => {
+ mockUseExtensionState.mockReturnValue({
+ taskHistory: mockTaskHistory,
+ cwd: "/workspace/project1",
+ taskTitlesEnabled: false,
+ } as any)
+
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ result.current.setSearchQuery("build")
+ })
+
+ expect(result.current.tasks).toHaveLength(0)
+ })
+
it("automatically switches to mostRelevant when searching", () => {
const { result } = renderHook(() => useTaskSearch())
diff --git a/webview-ui/src/components/history/useTaskSearch.ts b/webview-ui/src/components/history/useTaskSearch.ts
index 3c95f005e0..f8894617e3 100644
--- a/webview-ui/src/components/history/useTaskSearch.ts
+++ b/webview-ui/src/components/history/useTaskSearch.ts
@@ -7,7 +7,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
export const useTaskSearch = () => {
- const { taskHistory, cwd } = useExtensionState()
+ const { taskHistory, cwd, taskTitlesEnabled = false } = useExtensionState()
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState("newest")
@@ -33,9 +33,9 @@ export const useTaskSearch = () => {
const fzf = useMemo(() => {
return new Fzf(presentableTasks, {
- selector: (item) => (item.title ? `${item.title} ${item.task}` : item.task),
+ selector: (item) => (taskTitlesEnabled && item.title ? `${item.title} ${item.task}` : item.task),
})
- }, [presentableTasks])
+ }, [presentableTasks, taskTitlesEnabled])
const tasks = useMemo(() => {
let results = presentableTasks
@@ -44,11 +44,14 @@ export const useTaskSearch = () => {
const searchResults = fzf.find(searchQuery)
results = searchResults.map((result) => {
const positions = Array.from(result.positions)
- const titleLength = result.item.title ? result.item.title.length : 0
- const separatorLength = titleLength > 0 ? 1 : 0
- const taskOffset = titleLength + separatorLength
- const titlePositions = titleLength > 0 ? positions.filter((p) => p < titleLength) : []
- const taskPositions = positions.filter((p) => p >= taskOffset).map((p) => p - taskOffset)
+ const includeTitles = taskTitlesEnabled && !!result.item.title
+ const titleLength = includeTitles ? result.item.title!.length : 0
+ const separatorLength = includeTitles ? 1 : 0
+ const taskOffset = includeTitles ? titleLength + separatorLength : 0
+ const titlePositions = includeTitles ? positions.filter((p) => p < titleLength) : []
+ const taskPositions = includeTitles
+ ? positions.filter((p) => p >= taskOffset).map((p) => p - taskOffset)
+ : positions
const titleHighlight =
titlePositions.length > 0 && result.item.title
@@ -86,7 +89,7 @@ export const useTaskSearch = () => {
return (b.ts || 0) - (a.ts || 0)
}
})
- }, [presentableTasks, searchQuery, fzf, sortOption])
+ }, [presentableTasks, searchQuery, fzf, sortOption, taskTitlesEnabled])
return {
tasks,
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index d86007e80f..6ae051fdb5 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -214,6 +214,7 @@ const SettingsView = forwardRef(({ onDone, t
includeCurrentTime,
includeCurrentCost,
maxGitStatusFiles,
+ taskTitlesEnabled,
} = cachedState
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@@ -423,6 +424,7 @@ const SettingsView = forwardRef(({ onDone, t
condensingApiConfigId: condensingApiConfigId || "",
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
+ taskTitlesEnabled: taskTitlesEnabled ?? false,
enterBehavior: enterBehavior ?? "send",
includeCurrentTime: includeCurrentTime ?? true,
includeCurrentCost: includeCurrentCost ?? true,
@@ -910,6 +912,7 @@ const SettingsView = forwardRef(({ onDone, t
{/* UI Section */}
{renderTab === "ui" && (
{
+ taskTitlesEnabled: boolean
reasoningBlockCollapsed: boolean
enterBehavior: "send" | "newline"
setCachedStateField: SetCachedStateField
}
export const UISettings = ({
+ taskTitlesEnabled,
reasoningBlockCollapsed,
enterBehavior,
setCachedStateField,
@@ -28,6 +30,12 @@ export const UISettings = ({
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
return isMac ? "⌘" : "Ctrl"
}, [])
+ const handleTaskTitlesEnabledChange = (value: boolean) => {
+ setCachedStateField("taskTitlesEnabled", value)
+ telemetryClient.capture("ui_settings_task_titles_enabled_changed", {
+ enabled: value,
+ })
+ }
const handleReasoningBlockCollapsedChange = (value: boolean) => {
setCachedStateField("reasoningBlockCollapsed", value)
@@ -54,6 +62,18 @@ export const UISettings = ({
+
+
handleTaskTitlesEnabledChange(e.target.checked)}
+ data-testid="enable-task-titles-checkbox">
+ {t("settings:ui.taskTitles.label")}
+
+
+ {t("settings:ui.taskTitles.description")}
+
+
+
{/* Collapse Thinking Messages Setting */}
{
const defaultProps = {
+ taskTitlesEnabled: false,
reasoningBlockCollapsed: false,
enterBehavior: "send" as const,
setCachedStateField: vi.fn(),
@@ -15,6 +16,12 @@ describe("UISettings", () => {
expect(checkbox).toBeTruthy()
})
+ it("renders the task titles checkbox", () => {
+ const { getByTestId } = render()
+ const checkbox = getByTestId("enable-task-titles-checkbox")
+ expect(checkbox).toBeTruthy()
+ })
+
it("displays the correct initial state", () => {
const { getByTestId } = render()
const checkbox = getByTestId("collapse-thinking-checkbox") as HTMLInputElement
@@ -41,4 +48,18 @@ describe("UISettings", () => {
rerender()
expect(checkbox.checked).toBe(true)
})
+
+ it("calls setCachedStateField when task titles checkbox is toggled", async () => {
+ const setCachedStateField = vi.fn()
+ const { getByTestId } = render(
+ ,
+ )
+
+ const checkbox = getByTestId("enable-task-titles-checkbox")
+ fireEvent.click(checkbox)
+
+ await waitFor(() => {
+ expect(setCachedStateField).toHaveBeenCalledWith("taskTitlesEnabled", true)
+ })
+ })
})
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index d2ff79a8e0..ed4bb8eb77 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -148,6 +148,8 @@ export interface ExtensionStateContextType extends ExtensionState {
togglePinnedApiConfig: (configName: string) => void
terminalCompressProgressBar?: boolean
setTerminalCompressProgressBar: (value: boolean) => void
+ taskTitlesEnabled?: boolean
+ setTaskTitlesEnabled: (value: boolean) => void
setHistoryPreviewCollapsed: (value: boolean) => void
setReasoningBlockCollapsed: (value: boolean) => void
enterBehavior?: "send" | "newline"
@@ -253,6 +255,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
historyPreviewCollapsed: false, // Initialize the new state (default to expanded)
reasoningBlockCollapsed: true, // Default to collapsed
enterBehavior: "send", // Default: Enter sends, Shift+Enter creates newline
+ taskTitlesEnabled: false,
cloudUserInfo: null,
cloudIsAuthenticated: false,
cloudOrganizations: [],
@@ -454,6 +457,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const contextValue: ExtensionStateContextType = {
...state,
reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true,
+ taskTitlesEnabled: state.taskTitlesEnabled ?? false,
didHydrateState,
showWelcome,
theme,
@@ -574,6 +578,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setState((prevState) => ({ ...prevState, reasoningBlockCollapsed: value })),
enterBehavior: state.enterBehavior ?? "send",
setEnterBehavior: (value) => setState((prevState) => ({ ...prevState, enterBehavior: value })),
+ setTaskTitlesEnabled: (value) => setState((prevState) => ({ ...prevState, taskTitlesEnabled: value })),
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),
setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })),
setAutoCondenseContextPercent: (value) =>
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json
index fc64ad1851..56456f9b93 100644
--- a/webview-ui/src/i18n/locales/en/settings.json
+++ b/webview-ui/src/i18n/locales/en/settings.json
@@ -70,6 +70,10 @@
"description": "Manage your slash commands to quickly execute custom workflows and actions. Learn more"
},
"ui": {
+ "taskTitles": {
+ "label": "Enable editable task titles",
+ "description": "Show and edit custom titles for tasks in chat and history instead of only the original task text"
+ },
"collapseThinking": {
"label": "Collapse Thinking messages by default",
"description": "When enabled, thinking blocks will be collapsed by default until you interact with them"