Преглед изворни кода

feat: add task header highlight for visual status indication (#11305)

* feat: add task header highlight setting for visual status indication

Add a 'Task Header Highlight' toggle under Settings > UI that colors the
task header based on its current state:
- Green (--vscode-charts-green) when task completes (completion_result)
- Yellow (--vscode-charts-yellow) when user attention is needed (follow-up
  questions, tool approvals, etc.)

The highlight is skipped for subtasks and partial/streaming messages,
matching the same defensive logic used by sound notifications.

CSS classes with !important and --vscode-foreground variable overrides
ensure all child text, icons, SVGs, and the context progress bar use
appropriate contrasting colors.

Includes tests (37 passing) and translations for all 18 locales.

* fix: address review feedback - accessibility contrast and deduplicated logic

- Replace theme-dependent --vscode-charts-green/yellow backgrounds with
  hardcoded colors (#15803d, #ca8a04) that guarantee WCAG AA 4.5:1 contrast
- Extract shared lastRelevantMessage useMemo to deduplicate findLastIndex
  filtering between isTaskComplete and highlightClass
Hannes Rudolph пре 4 дана
родитељ
комит
5313cb503a
28 измењених фајлова са 434 додато и 24 уклоњено
  1. 2 0
      packages/types/src/global-settings.ts
  2. 1 0
      packages/types/src/vscode-extension-host.ts
  3. 3 0
      src/core/webview/ClineProvider.ts
  4. 31 16
      webview-ui/src/components/chat/TaskHeader.tsx
  5. 172 0
      webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
  6. 12 4
      webview-ui/src/components/settings/SettingsView.tsx
  7. 33 4
      webview-ui/src/components/settings/UISettings.tsx
  8. 79 0
      webview-ui/src/components/settings/__tests__/UISettings.spec.tsx
  9. 2 0
      webview-ui/src/context/ExtensionStateContext.tsx
  10. 4 0
      webview-ui/src/i18n/locales/ca/settings.json
  11. 4 0
      webview-ui/src/i18n/locales/de/settings.json
  12. 4 0
      webview-ui/src/i18n/locales/en/settings.json
  13. 4 0
      webview-ui/src/i18n/locales/es/settings.json
  14. 4 0
      webview-ui/src/i18n/locales/fr/settings.json
  15. 4 0
      webview-ui/src/i18n/locales/hi/settings.json
  16. 4 0
      webview-ui/src/i18n/locales/id/settings.json
  17. 4 0
      webview-ui/src/i18n/locales/it/settings.json
  18. 4 0
      webview-ui/src/i18n/locales/ja/settings.json
  19. 4 0
      webview-ui/src/i18n/locales/ko/settings.json
  20. 4 0
      webview-ui/src/i18n/locales/nl/settings.json
  21. 4 0
      webview-ui/src/i18n/locales/pl/settings.json
  22. 4 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  23. 4 0
      webview-ui/src/i18n/locales/ru/settings.json
  24. 4 0
      webview-ui/src/i18n/locales/tr/settings.json
  25. 4 0
      webview-ui/src/i18n/locales/vi/settings.json
  26. 4 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  27. 4 0
      webview-ui/src/i18n/locales/zh-TW/settings.json
  28. 27 0
      webview-ui/src/index.css

+ 2 - 0
packages/types/src/global-settings.ts

@@ -167,6 +167,7 @@ export const globalSettingsSchema = z.object({
 	ttsSpeed: z.number().optional(),
 	soundEnabled: z.boolean().optional(),
 	soundVolume: z.number().optional(),
+	taskHeaderHighlightEnabled: z.boolean().optional(),
 
 	maxOpenTabsContext: z.number().optional(),
 	maxWorkspaceFiles: z.number().optional(),
@@ -368,6 +369,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
 	ttsSpeed: 1,
 	soundEnabled: false,
 	soundVolume: 0.5,
+	taskHeaderHighlightEnabled: false,
 
 	terminalShellIntegrationTimeout: 30000,
 	terminalCommandDelay: 0,

+ 1 - 0
packages/types/src/vscode-extension-host.ts

@@ -303,6 +303,7 @@ export type ExtensionState = Pick<
 	| "ttsSpeed"
 	| "soundEnabled"
 	| "soundVolume"
+	| "taskHeaderHighlightEnabled"
 	| "terminalOutputPreviewSize"
 	| "terminalShellIntegrationTimeout"
 	| "terminalShellIntegrationDisabled"

+ 3 - 0
src/core/webview/ClineProvider.ts

@@ -2060,6 +2060,7 @@ export class ClineProvider
 			historyPreviewCollapsed,
 			reasoningBlockCollapsed,
 			enterBehavior,
+			taskHeaderHighlightEnabled,
 			cloudUserInfo,
 			cloudIsAuthenticated,
 			sharingEnabled,
@@ -2204,6 +2205,7 @@ export class ClineProvider
 			historyPreviewCollapsed: historyPreviewCollapsed ?? false,
 			reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
 			enterBehavior: enterBehavior ?? "send",
+			taskHeaderHighlightEnabled: taskHeaderHighlightEnabled ?? false,
 			cloudUserInfo,
 			cloudIsAuthenticated: cloudIsAuthenticated ?? false,
 			cloudAuthSkipModel: this.context.globalState.get<boolean>("roo-auth-skip-model") ?? false,
@@ -2442,6 +2444,7 @@ export class ClineProvider
 			historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
 			reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
 			enterBehavior: stateValues.enterBehavior ?? "send",
+			taskHeaderHighlightEnabled: stateValues.taskHeaderHighlightEnabled ?? false,
 			cloudUserInfo,
 			cloudIsAuthenticated,
 			sharingEnabled,

+ 31 - 16
webview-ui/src/components/chat/TaskHeader.tsx

@@ -68,7 +68,8 @@ const TaskHeader = ({
 	todos,
 }: TaskHeaderProps) => {
 	const { t } = useTranslation()
-	const { apiConfiguration, currentTaskItem, clineMessages, isBrowserSessionActive } = useExtensionState()
+	const { apiConfiguration, currentTaskItem, clineMessages, isBrowserSessionActive, taskHeaderHighlightEnabled } =
+		useExtensionState()
 	const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
 	const [isTaskExpanded, setIsTaskExpanded] = useState(false)
 	const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false)
@@ -76,19 +77,34 @@ const TaskHeader = ({
 		autoOpenOnAuth: false,
 	})
 
+	// Determine if this is a subtask (has a parent)
+	const isSubtask = !!parentTaskId
+
+	// Find the last message that isn't a resume action (shared by isTaskComplete and highlightClass)
+	const lastRelevantMessage = useMemo(() => {
+		const msgs = clineMessages || []
+		const idx = findLastIndex(msgs, (m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))
+		return idx !== -1 ? msgs[idx] : undefined
+	}, [clineMessages])
+
 	// Check if the task is complete by looking at the last relevant message (skipping resume messages)
-	const isTaskComplete =
-		clineMessages && clineMessages.length > 0
-			? (() => {
-					const lastRelevantIndex = findLastIndex(
-						clineMessages,
-						(m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
-					)
-					return lastRelevantIndex !== -1
-						? clineMessages[lastRelevantIndex]?.ask === "completion_result"
-						: false
-				})()
-			: false
+	const isTaskComplete = lastRelevantMessage?.ask === "completion_result"
+
+	// Compute highlight CSS class: green for task complete, yellow for user attention needed
+	const highlightClass = useMemo(() => {
+		if (!taskHeaderHighlightEnabled || isSubtask) return undefined
+		if (!lastRelevantMessage || lastRelevantMessage.partial) return undefined
+
+		if (lastRelevantMessage.ask === "completion_result") {
+			return "task-header-highlight-green"
+		}
+
+		if (lastRelevantMessage.ask) {
+			return "task-header-highlight-yellow"
+		}
+
+		return undefined
+	}, [taskHeaderHighlightEnabled, isSubtask, lastRelevantMessage])
 
 	useEffect(() => {
 		const timer = setTimeout(() => {
@@ -141,9 +157,6 @@ const TaskHeader = ({
 
 	const hasTodos = todos && Array.isArray(todos) && todos.length > 0
 
-	// Determine if this is a subtask (has a parent)
-	const isSubtask = !!parentTaskId
-
 	const handleBackToParent = () => {
 		if (parentTaskId) {
 			vscode.postMessage({ type: "showTaskWithId", text: parentTaskId })
@@ -174,12 +187,14 @@ const TaskHeader = ({
 				</DismissibleUpsell>
 			)}
 			<div
+				data-testid="task-header-container"
 				className={cn(
 					"px-3 pt-2.5 pb-2 flex flex-col gap-1.5 relative z-1 cursor-pointer",
 					"bg-vscode-input-background hover:bg-vscode-input-background/90",
 					"text-vscode-foreground/80 hover:text-vscode-foreground",
 					"shadow-lg shadow-vscode-sideBar-background/50 rounded-xl",
 					hasTodos && "border-b-0",
+					highlightClass,
 				)}
 				onClick={(e) => {
 					// Don't expand if clicking on todos section

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

@@ -40,6 +40,7 @@ let mockExtensionState: {
 	apiConfiguration: ProviderSettings
 	currentTaskItem: { id: string } | null
 	clineMessages: any[]
+	taskHeaderHighlightEnabled?: boolean
 } = {
 	apiConfiguration: {
 		apiProvider: "anthropic",
@@ -48,6 +49,7 @@ let mockExtensionState: {
 	} as ProviderSettings,
 	currentTaskItem: { id: "test-task-id" },
 	clineMessages: [],
+	taskHeaderHighlightEnabled: false,
 }
 
 // Mock the ExtensionStateContext
@@ -215,6 +217,7 @@ describe("TaskHeader", () => {
 				} as ProviderSettings,
 				currentTaskItem: { id: "test-task-id" },
 				clineMessages: [],
+				taskHeaderHighlightEnabled: false,
 			}
 		})
 
@@ -423,6 +426,175 @@ describe("TaskHeader", () => {
 		})
 	})
 
+	describe("Task header highlight", () => {
+		const completionMessages = [
+			{
+				type: "ask",
+				ask: "completion_result",
+				ts: Date.now(),
+				text: "Task completed!",
+			},
+		]
+
+		beforeEach(() => {
+			mockExtensionState = {
+				apiConfiguration: {
+					apiProvider: "anthropic",
+					apiKey: "test-api-key",
+					apiModelId: "claude-3-opus-20240229",
+				} as ProviderSettings,
+				currentTaskItem: { id: "test-task-id" },
+				clineMessages: [],
+				taskHeaderHighlightEnabled: false,
+			}
+		})
+
+		it("should apply green highlight class when task is complete and highlight is enabled", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: completionMessages,
+				taskHeaderHighlightEnabled: true,
+			}
+
+			renderTaskHeader()
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-green")).toBe(true)
+			expect(container.classList.contains("task-header-highlight-yellow")).toBe(false)
+		})
+
+		it("should apply yellow highlight class when task needs user attention and highlight is enabled", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: [
+					{
+						type: "ask",
+						ask: "tool",
+						ts: Date.now(),
+						text: "Need permission to use tool",
+					},
+				],
+				taskHeaderHighlightEnabled: true,
+			}
+
+			renderTaskHeader()
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-yellow")).toBe(true)
+			expect(container.classList.contains("task-header-highlight-green")).toBe(false)
+		})
+
+		it("should not apply highlight when highlight is disabled", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: completionMessages,
+				taskHeaderHighlightEnabled: false,
+			}
+
+			renderTaskHeader()
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-green")).toBe(false)
+			expect(container.classList.contains("task-header-highlight-yellow")).toBe(false)
+		})
+
+		it("should not apply highlight when task is a subtask", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: completionMessages,
+				taskHeaderHighlightEnabled: true,
+			}
+
+			renderTaskHeader({ parentTaskId: "parent-task-123" })
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-green")).toBe(false)
+			expect(container.classList.contains("task-header-highlight-yellow")).toBe(false)
+		})
+
+		it("should not apply highlight when last message is partial", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: [
+					{
+						type: "ask",
+						ask: "completion_result",
+						ts: Date.now(),
+						text: "Task completed!",
+						partial: true,
+					},
+				],
+				taskHeaderHighlightEnabled: true,
+			}
+
+			renderTaskHeader()
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-green")).toBe(false)
+			expect(container.classList.contains("task-header-highlight-yellow")).toBe(false)
+		})
+
+		it("should not apply highlight when no clineMessages exist", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: [],
+				taskHeaderHighlightEnabled: true,
+			}
+
+			renderTaskHeader()
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-green")).toBe(false)
+			expect(container.classList.contains("task-header-highlight-yellow")).toBe(false)
+		})
+
+		it("should not apply highlight when last relevant message has no ask type", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: [{ type: "say", say: "text", ts: Date.now(), text: "Working..." }],
+				taskHeaderHighlightEnabled: true,
+			}
+
+			renderTaskHeader()
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-green")).toBe(false)
+			expect(container.classList.contains("task-header-highlight-yellow")).toBe(false)
+		})
+
+		it("should apply green class when completion_result is followed by resume messages", () => {
+			mockExtensionState = {
+				...mockExtensionState,
+				clineMessages: [
+					{
+						type: "ask",
+						ask: "completion_result",
+						ts: Date.now() - 2000,
+						text: "Task completed!",
+					},
+					{
+						type: "ask",
+						ask: "resume_completed_task",
+						ts: Date.now() - 1000,
+						text: "Resume completed task?",
+					},
+					{
+						type: "ask",
+						ask: "resume_task",
+						ts: Date.now(),
+						text: "Resume task?",
+					},
+				],
+				taskHeaderHighlightEnabled: true,
+			}
+
+			renderTaskHeader()
+
+			const container = screen.getByTestId("task-header-container")
+			expect(container.classList.contains("task-header-highlight-green")).toBe(true)
+		})
+	})
+
 	describe("Context window percentage calculation", () => {
 		// The percentage should be calculated as:
 		// contextTokens / (contextWindow - reservedForOutput) * 100

+ 12 - 4
webview-ui/src/components/settings/SettingsView.tsx

@@ -124,6 +124,11 @@ type SettingsViewProps = {
 	targetSection?: string
 }
 
+const withCachedStateDefaults = (state: ExtensionStateContextType): ExtensionStateContextType => ({
+	...state,
+	taskHeaderHighlightEnabled: state.taskHeaderHighlightEnabled ?? false,
+})
+
 const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, targetSection }, ref) => {
 	const { t } = useAppTranslation()
 
@@ -147,7 +152,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 	const prevApiConfigName = useRef(currentApiConfigName)
 	const confirmDialogHandler = useRef<() => void>()
 
-	const [cachedState, setCachedState] = useState(() => extensionState)
+	const [cachedState, setCachedState] = useState(() => withCachedStateDefaults(extensionState))
 
 	const {
 		alwaysAllowReadOnly,
@@ -212,6 +217,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 		includeCurrentTime,
 		includeCurrentCost,
 		maxGitStatusFiles,
+		taskHeaderHighlightEnabled,
 	} = cachedState
 
 	const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@@ -223,7 +229,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 			return
 		}
 
-		setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
+		setCachedState((prevCachedState) => withCachedStateDefaults({ ...prevCachedState, ...extensionState }))
 		prevApiConfigName.current = currentApiConfigName
 		setChangeDetected(false)
 	}, [currentApiConfigName, extensionState])
@@ -231,7 +237,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 	// Bust the cache when settings are imported.
 	useEffect(() => {
 		if (settingsImportedAt) {
-			setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
+			setCachedState((prevCachedState) => withCachedStateDefaults({ ...prevCachedState, ...extensionState }))
 			setChangeDetected(false)
 		}
 	}, [settingsImportedAt, extensionState])
@@ -415,6 +421,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 					includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
 					reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
 					enterBehavior: enterBehavior ?? "send",
+					taskHeaderHighlightEnabled,
 					includeCurrentTime: includeCurrentTime ?? true,
 					includeCurrentCost: includeCurrentCost ?? true,
 					maxGitStatusFiles: maxGitStatusFiles ?? 0,
@@ -455,7 +462,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 		(confirm: boolean) => {
 			if (confirm) {
 				// Discard changes: Reset state and flag
-				setCachedState(extensionState) // Revert to original state
+				setCachedState(withCachedStateDefaults(extensionState)) // Revert to original state
 				setChangeDetected(false) // Reset change flag
 				confirmDialogHandler.current?.() // Execute the pending action (e.g., tab switch)
 			}
@@ -906,6 +913,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 						{/* UI Section */}
 						{renderTab === "ui" && (
 							<UISettings
+								taskHeaderHighlightEnabled={taskHeaderHighlightEnabled ?? false}
 								reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
 								enterBehavior={enterBehavior ?? "send"}
 								setCachedStateField={setCachedStateField}

+ 33 - 4
webview-ui/src/components/settings/UISettings.tsx

@@ -7,17 +7,18 @@ import { SetCachedStateField } from "./types"
 import { SectionHeader } from "./SectionHeader"
 import { Section } from "./Section"
 import { SearchableSetting } from "./SearchableSetting"
-import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
 
 interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
 	reasoningBlockCollapsed: boolean
 	enterBehavior: "send" | "newline"
-	setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
+	taskHeaderHighlightEnabled: boolean
+	setCachedStateField: SetCachedStateField<"reasoningBlockCollapsed" | "enterBehavior" | "taskHeaderHighlightEnabled">
 }
 
 export const UISettings = ({
 	reasoningBlockCollapsed,
 	enterBehavior,
+	taskHeaderHighlightEnabled,
 	setCachedStateField,
 	...props
 }: UISettingsProps) => {
@@ -48,12 +49,36 @@ export const UISettings = ({
 		})
 	}
 
+	const handleTaskHeaderHighlightChange = (enabled: boolean) => {
+		setCachedStateField("taskHeaderHighlightEnabled", enabled)
+	}
+
 	return (
 		<div {...props}>
 			<SectionHeader>{t("settings:sections.ui")}</SectionHeader>
 
 			<Section>
 				<div className="space-y-6">
+					{/* Task Header Highlight Setting */}
+					<SearchableSetting
+						settingId="ui-task-complete-color"
+						section="ui"
+						label={t("settings:ui.taskCompleteColor.label")}>
+						<div className="flex flex-col gap-1">
+							<VSCodeCheckbox
+								checked={taskHeaderHighlightEnabled}
+								onChange={(event) =>
+									handleTaskHeaderHighlightChange((event.target as HTMLInputElement).checked)
+								}
+								data-testid="task-header-highlight-checkbox">
+								<span className="font-medium">{t("settings:ui.taskCompleteColor.label")}</span>
+							</VSCodeCheckbox>
+							<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
+								{t("settings:ui.taskCompleteColor.description")}
+							</div>
+						</div>
+					</SearchableSetting>
+
 					{/* Collapse Thinking Messages Setting */}
 					<SearchableSetting
 						settingId="ui-collapse-thinking"
@@ -62,7 +87,9 @@ export const UISettings = ({
 						<div className="flex flex-col gap-1">
 							<VSCodeCheckbox
 								checked={reasoningBlockCollapsed}
-								onChange={(e: any) => handleReasoningBlockCollapsedChange(e.target.checked)}
+								onChange={(event) =>
+									handleReasoningBlockCollapsedChange((event.target as HTMLInputElement).checked)
+								}
 								data-testid="collapse-thinking-checkbox">
 								<span className="font-medium">{t("settings:ui.collapseThinking.label")}</span>
 							</VSCodeCheckbox>
@@ -80,7 +107,9 @@ export const UISettings = ({
 						<div className="flex flex-col gap-1">
 							<VSCodeCheckbox
 								checked={enterBehavior === "newline"}
-								onChange={(e: any) => handleEnterBehaviorChange(e.target.checked)}
+								onChange={(event) =>
+									handleEnterBehaviorChange((event.target as HTMLInputElement).checked)
+								}
 								data-testid="enter-behavior-checkbox">
 								<span className="font-medium">
 									{t("settings:ui.requireCtrlEnterToSend.label", { primaryMod })}

+ 79 - 0
webview-ui/src/components/settings/__tests__/UISettings.spec.tsx

@@ -2,10 +2,38 @@ import { render, fireEvent, waitFor } from "@testing-library/react"
 import { describe, it, expect, vi } from "vitest"
 import { UISettings } from "../UISettings"
 
+// Mock useAppTranslation
+vi.mock("@/i18n/TranslationContext", () => ({
+	useAppTranslation: () => ({
+		t: (key: string) => key,
+	}),
+}))
+
+// Mock telemetry
+vi.mock("@/utils/TelemetryClient", () => ({
+	telemetryClient: { capture: vi.fn() },
+}))
+
+// Mock SearchableSetting to render children directly
+vi.mock("../SearchableSetting", () => ({
+	SearchableSetting: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
+}))
+
+// Mock SectionHeader to render children
+vi.mock("../SectionHeader", () => ({
+	SectionHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
+}))
+
+// Mock Section to render children
+vi.mock("../Section", () => ({
+	Section: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
+}))
+
 describe("UISettings", () => {
 	const defaultProps = {
 		reasoningBlockCollapsed: false,
 		enterBehavior: "send" as const,
+		taskHeaderHighlightEnabled: false,
 		setCachedStateField: vi.fn(),
 	}
 
@@ -41,4 +69,55 @@ describe("UISettings", () => {
 		rerender(<UISettings {...defaultProps} reasoningBlockCollapsed={true} />)
 		expect(checkbox.checked).toBe(true)
 	})
+
+	describe("Task header highlight", () => {
+		it("renders checkbox unchecked when taskHeaderHighlightEnabled is false", () => {
+			const { getByTestId } = render(<UISettings {...defaultProps} taskHeaderHighlightEnabled={false} />)
+			const checkbox = getByTestId("task-header-highlight-checkbox") as HTMLInputElement
+			expect(checkbox).toBeTruthy()
+			expect(checkbox.checked).toBe(false)
+		})
+
+		it("renders checkbox checked when taskHeaderHighlightEnabled is true", () => {
+			const { getByTestId } = render(<UISettings {...defaultProps} taskHeaderHighlightEnabled={true} />)
+			const checkbox = getByTestId("task-header-highlight-checkbox") as HTMLInputElement
+			expect(checkbox.checked).toBe(true)
+		})
+
+		it("calls setCachedStateField with true when toggling on", async () => {
+			const setCachedStateField = vi.fn()
+			const { getByTestId } = render(
+				<UISettings
+					{...defaultProps}
+					taskHeaderHighlightEnabled={false}
+					setCachedStateField={setCachedStateField}
+				/>,
+			)
+
+			const checkbox = getByTestId("task-header-highlight-checkbox")
+			fireEvent.click(checkbox)
+
+			await waitFor(() => {
+				expect(setCachedStateField).toHaveBeenCalledWith("taskHeaderHighlightEnabled", true)
+			})
+		})
+
+		it("calls setCachedStateField with false when toggling off", async () => {
+			const setCachedStateField = vi.fn()
+			const { getByTestId } = render(
+				<UISettings
+					{...defaultProps}
+					taskHeaderHighlightEnabled={true}
+					setCachedStateField={setCachedStateField}
+				/>,
+			)
+
+			const checkbox = getByTestId("task-header-highlight-checkbox")
+			fireEvent.click(checkbox)
+
+			await waitFor(() => {
+				expect(setCachedStateField).toHaveBeenCalledWith("taskHeaderHighlightEnabled", false)
+			})
+		})
+	})
 })

+ 2 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -139,6 +139,7 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setReasoningBlockCollapsed: (value: boolean) => void
 	enterBehavior?: "send" | "newline"
 	setEnterBehavior: (value: "send" | "newline") => void
+	taskHeaderHighlightEnabled?: boolean
 	autoCondenseContext: boolean
 	setAutoCondenseContext: (value: boolean) => void
 	autoCondenseContextPercent: number
@@ -238,6 +239,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
+		taskHeaderHighlightEnabled: false,
 		cloudUserInfo: null,
 		cloudIsAuthenticated: false,
 		cloudOrganizations: [],

+ 4 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Ressaltat de la capçalera de tasca",
+			"description": "Ressalta la capçalera de la tasca amb un indicador de color quan les tasques es completen o necessiten la teva atenció, ajudant-te a detectar ràpidament quan cal actuar"
+		},
 		"collapseThinking": {
 			"label": "Replega els missatges de pensament per defecte",
 			"description": "Quan estigui activat, els blocs de pensament es replegaran per defecte fins que interactuïs amb ells"

+ 4 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Aufgaben-Header-Hervorhebung",
+			"description": "Hebt den Aufgaben-Header mit einem Farbindikator hervor, wenn Aufgaben abgeschlossen werden oder deine Aufmerksamkeit erfordern, damit du schnell erkennst, wann eine Aktion nötig ist"
+		},
 		"collapseThinking": {
 			"label": "Gedankenblöcke standardmäßig ausblenden",
 			"description": "Wenn aktiviert, werden Gedankenblöcke standardmäßig ausgeblendet, bis du mit ihnen interagierst"

+ 4 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -157,6 +157,10 @@
 		"footer": "Create your own skills with the Skill Writer mode, available in the <MarketplaceLink>Modes Marketplace</MarketplaceLink>."
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Task Header Highlight",
+			"description": "Highlights the task header with a color indicator when tasks complete or need your attention, helping you quickly spot when action is required"
+		},
 		"collapseThinking": {
 			"label": "Collapse Thinking messages by default",
 			"description": "When enabled, thinking blocks will be collapsed by default until you interact with them"

+ 4 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Resaltado del encabezado de tarea",
+			"description": "Resalta el encabezado de la tarea con un indicador de color cuando las tareas se completan o necesitan tu atención, ayudándote a detectar rápidamente cuándo se requiere acción"
+		},
 		"collapseThinking": {
 			"label": "Colapsar mensajes de pensamiento por defecto",
 			"description": "Cuando está activado, los bloques de pensamiento se colapsarán por defecto hasta que interactúes con ellos"

+ 4 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Surlignage de l'en-tête de tâche",
+			"description": "Surligne l'en-tête de la tâche avec un indicateur de couleur lorsque les tâches sont terminées ou nécessitent ton attention, t'aidant à repérer rapidement quand une action est requise"
+		},
 		"collapseThinking": {
 			"label": "Réduire les messages de réflexion par défaut",
 			"description": "Si activé, les blocs de réflexion seront réduits par défaut jusqu'à ce que vous interagissiez avec eux"

+ 4 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "टास्क हेडर हाइलाइट",
+			"description": "जब टास्क पूरे होते हैं या तुम्हारे ध्यान की ज़रूरत होती है, तो टास्क हेडर को रंग संकेतक से हाइलाइट करता है, ताकि तुम जल्दी से पहचान सको कि कब कार्रवाई ज़रूरी है"
+		},
 		"collapseThinking": {
 			"label": "सोच संदेशों को डिफ़ॉल्ट रूप से संक्षिप्त करें",
 			"description": "सक्षम होने पर, सोच ब्लॉक आपके द्वारा उनके साथ इंटरैक्ट करने तक डिफ़ॉल्ट रूप से संक्षिप्त रहेंगे"

+ 4 - 0
webview-ui/src/i18n/locales/id/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Sorotan Header Tugas",
+			"description": "Menyoroti header tugas dengan indikator warna saat tugas selesai atau membutuhkan perhatianmu, membantumu dengan cepat mengetahui kapan tindakan diperlukan"
+		},
 		"collapseThinking": {
 			"label": "Ciutkan pesan Berpikir secara default",
 			"description": "Jika diaktifkan, blok berpikir akan diciutkan secara default sampai Anda berinteraksi dengannya"

+ 4 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Evidenziazione intestazione attività",
+			"description": "Evidenzia l'intestazione dell'attività con un indicatore di colore quando le attività vengono completate o richiedono la tua attenzione, aiutandoti a individuare rapidamente quando è necessaria un'azione"
+		},
 		"collapseThinking": {
 			"label": "Comprimi i messaggi di pensiero per impostazione predefinita",
 			"description": "Se abilitato, i blocchi di pensiero verranno compressi per impostazione predefinita finché non interagisci con essi"

+ 4 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "タスクヘッダーのハイライト",
+			"description": "タスクが完了したり注意が必要なときに、タスクヘッダーをカラーインジケーターでハイライトし、アクションが必要なタイミングをすばやく把握できるようにします"
+		},
 		"collapseThinking": {
 			"label": "デフォルトで思考メッセージを折りたたむ",
 			"description": "有効にすると、操作するまで思考ブロックがデフォルトで折りたたまれます"

+ 4 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "작업 헤더 하이라이트",
+			"description": "작업이 완료되거나 주의가 필요할 때 작업 헤더를 색상 표시로 강조하여 조치가 필요한 시점을 빠르게 파악할 수 있도록 합니다"
+		},
 		"collapseThinking": {
 			"label": "기본적으로 생각 메시지 접기",
 			"description": "활성화하면 상호 작용할 때까지 생각 블록이 기본적으로 접힙니다"

+ 4 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Taakkop markering",
+			"description": "Markeert de taakkop met een kleurindicator wanneer taken voltooid zijn of je aandacht nodig hebben, zodat je snel kunt zien wanneer actie vereist is"
+		},
 		"collapseThinking": {
 			"label": "Denkberichten standaard samenvouwen",
 			"description": "Indien ingeschakeld, worden denkblokken standaard samengevouwen totdat je ermee interageert"

+ 4 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Podświetlenie nagłówka zadania",
+			"description": "Podświetla nagłówek zadania kolorowym wskaźnikiem, gdy zadania zostaną ukończone lub wymagają twojej uwagi, pomagając szybko zauważyć, kiedy wymagane jest działanie"
+		},
 		"collapseThinking": {
 			"label": "Domyślnie zwijaj komunikaty o myśleniu",
 			"description": "Gdy włączone, bloki myślenia będą domyślnie zwinięte, dopóki nie wejdziesz z nimi w interakcję"

+ 4 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Destaque do cabeçalho da tarefa",
+			"description": "Destaca o cabeçalho da tarefa com um indicador de cor quando as tarefas são concluídas ou precisam da sua atenção, ajudando você a identificar rapidamente quando uma ação é necessária"
+		},
 		"collapseThinking": {
 			"label": "Recolher mensagens de pensamento por padrão",
 			"description": "Quando ativado, os blocos de pensamento serão recolhidos por padrão até que você interaja com eles"

+ 4 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Подсветка заголовка задачи",
+			"description": "Подсвечивает заголовок задачи цветовым индикатором при завершении задач или когда требуется твоё внимание, помогая быстро заметить, когда необходимо действие"
+		},
 		"collapseThinking": {
 			"label": "Сворачивать сообщения о размышлениях по умолчанию",
 			"description": "Если включено, блоки с размышлениями будут свернуты по умолчанию, пока вы не начнете с ними взаимодействовать"

+ 4 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Görev Başlığı Vurgulama",
+			"description": "Görevler tamamlandığında veya dikkatine ihtiyaç duyulduğunda görev başlığını renk göstergesiyle vurgular, eylem gerektiğinde hızlıca fark etmene yardımcı olur"
+		},
 		"collapseThinking": {
 			"label": "Düşünme mesajlarını varsayılan olarak daralt",
 			"description": "Etkinleştirildiğinde, düşünme blokları siz onlarla etkileşime girene kadar varsayılan olarak daraltılır"

+ 4 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "Đánh dấu tiêu đề tác vụ",
+			"description": "Đánh dấu tiêu đề tác vụ bằng chỉ báo màu khi tác vụ hoàn thành hoặc cần sự chú ý của bạn, giúp bạn nhanh chóng nhận biết khi nào cần hành động"
+		},
 		"collapseThinking": {
 			"label": "Thu gọn tin nhắn Suy nghĩ theo mặc định",
 			"description": "Khi được bật, các khối suy nghĩ sẽ được thu gọn theo mặc định cho đến khi bạn tương tác với chúng"

+ 4 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -1005,6 +1005,10 @@
 		}
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "任务标题高亮",
+			"description": "当任务完成或需要你关注时,通过颜色指示器高亮任务标题,帮助你快速发现需要操作的时机"
+		},
 		"collapseThinking": {
 			"label": "默认折叠「思考」消息",
 			"description": "启用后,「思考」块将默认折叠,直到您与其交互"

+ 4 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -104,6 +104,10 @@
 		"footer": "使用斜線命令快速存取經常使用的提示詞和工作流程。"
 	},
 	"ui": {
+		"taskCompleteColor": {
+			"label": "工作標題醒目提示",
+			"description": "當工作完成或需要你注意時,以顏色指示器醒目提示工作標題,幫助你快速發現需要採取行動的時機"
+		},
 		"collapseThinking": {
 			"label": "預設折疊「思考」訊息",
 			"description": "啟用後,「思考」塊將預設折疊,直到您與其互動"

+ 27 - 0
webview-ui/src/index.css

@@ -586,3 +586,30 @@ input[cmdk-input]:focus {
 	padding: 8px;
 	margin: -8px;
 }
+
+/* Task header highlight overrides - force all children to use contrasting text.
+   Uses hardcoded background colors instead of theme-dependent --vscode-charts-*
+   variables to guarantee WCAG AA 4.5:1 contrast with text colors. */
+.task-header-highlight-green {
+	--vscode-foreground: white;
+	background-color: #15803d !important; /* green-700, WCAG AA ~6.5:1 with white */
+}
+.task-header-highlight-green,
+.task-header-highlight-green * {
+	color: white !important;
+}
+.task-header-highlight-green svg {
+	stroke: white !important;
+}
+
+.task-header-highlight-yellow {
+	--vscode-foreground: black;
+	background-color: #ca8a04 !important; /* yellow-600, WCAG AA ~6.2:1 with black */
+}
+.task-header-highlight-yellow,
+.task-header-highlight-yellow * {
+	color: black !important;
+}
+.task-header-highlight-yellow svg {
+	stroke: black !important;
+}