Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -368,6 +369,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
ttsSpeed: 1,
soundEnabled: false,
soundVolume: 0.5,
taskHeaderHighlightEnabled: false,

terminalShellIntegrationTimeout: 30000,
terminalCommandDelay: 0,
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export type ExtensionState = Pick<
| "ttsSpeed"
| "soundEnabled"
| "soundVolume"
| "taskHeaderHighlightEnabled"
| "terminalOutputPreviewSize"
| "terminalShellIntegrationTimeout"
| "terminalShellIntegrationDisabled"
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2064,6 +2064,7 @@ export class ClineProvider
historyPreviewCollapsed,
reasoningBlockCollapsed,
enterBehavior,
taskHeaderHighlightEnabled,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
Expand Down Expand Up @@ -2208,6 +2209,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,
Expand Down Expand Up @@ -2446,6 +2448,7 @@ export class ClineProvider
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
enterBehavior: stateValues.enterBehavior ?? "send",
taskHeaderHighlightEnabled: stateValues.taskHeaderHighlightEnabled ?? false,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
Expand Down
47 changes: 31 additions & 16 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,43 @@ 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)
const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({
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(() => {
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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
Expand Down
172 changes: 172 additions & 0 deletions webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ let mockExtensionState: {
apiConfiguration: ProviderSettings
currentTaskItem: { id: string } | null
clineMessages: any[]
taskHeaderHighlightEnabled?: boolean
} = {
apiConfiguration: {
apiProvider: "anthropic",
Expand All @@ -48,6 +49,7 @@ let mockExtensionState: {
} as ProviderSettings,
currentTaskItem: { id: "test-task-id" },
clineMessages: [],
taskHeaderHighlightEnabled: false,
}

// Mock the ExtensionStateContext
Expand Down Expand Up @@ -215,6 +217,7 @@ describe("TaskHeader", () => {
} as ProviderSettings,
currentTaskItem: { id: "test-task-id" },
clineMessages: [],
taskHeaderHighlightEnabled: false,
}
})

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading