chore: merge origin/main into vk/6325-multi-question and resolve conflicts

This commit is contained in:
ScDor 2026-02-10 00:40:35 +02:00
commit c947059979
31 changed files with 870 additions and 1531 deletions

View file

@ -167,7 +167,6 @@ 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(),
@ -230,6 +229,12 @@ export const globalSettingsSchema = z.object({
*/
showQuestionsOneByOne: z.boolean().optional(),
/**
* Whether to highlight the task header in the chat view.
* @default false
*/
taskHeaderHighlightEnabled: z.boolean().optional(),
/**
* Path to worktree to auto-open after switching workspaces.
* Used by the worktree feature to open the Roo Code sidebar in a new window.
@ -367,7 +372,6 @@ export const EVALS_SETTINGS: RooCodeSettings = {
ttsSpeed: 1,
soundEnabled: false,
soundVolume: 0.5,
taskHeaderHighlightEnabled: false,
terminalShellIntegrationTimeout: 30000,
terminalCommandDelay: 0,
@ -400,6 +404,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
customModes: [],
showQuestionsOneByOne: false,
taskHeaderHighlightEnabled: false,
}
export const EVALS_TIMEOUT = 5 * 60 * 1_000

View file

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

View file

@ -0,0 +1,281 @@
// npx vitest run __tests__/removeClineFromStack-delegation.spec.ts
import { describe, it, expect, vi } from "vitest"
import { ClineProvider } from "../core/webview/ClineProvider"
describe("ClineProvider.removeClineFromStack() delegation awareness", () => {
/**
* Helper to build a minimal mock provider with a single task on the stack.
* The task's parentTaskId and taskId are configurable.
*/
function buildMockProvider(opts: {
childTaskId: string
parentTaskId?: string
parentHistoryItem?: Record<string, any>
getTaskWithIdError?: Error
}) {
const childTask = {
taskId: opts.childTaskId,
instanceId: "inst-1",
parentTaskId: opts.parentTaskId,
emit: vi.fn(),
abortTask: vi.fn().mockResolvedValue(undefined),
}
const updateTaskHistory = vi.fn().mockResolvedValue([])
const getTaskWithId = opts.getTaskWithIdError
? vi.fn().mockRejectedValue(opts.getTaskWithIdError)
: vi.fn().mockImplementation(async (id: string) => {
if (id === opts.parentTaskId && opts.parentHistoryItem) {
return { historyItem: { ...opts.parentHistoryItem } }
}
throw new Error("Task not found")
})
const provider = {
clineStack: [childTask] as any[],
taskEventListeners: new Map(),
log: vi.fn(),
getTaskWithId,
updateTaskHistory,
}
return { provider, childTask, updateTaskHistory, getTaskWithId }
}
it("repairs parent metadata (delegated → active) when a delegated child is removed", async () => {
const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({
childTaskId: "child-1",
parentTaskId: "parent-1",
parentHistoryItem: {
id: "parent-1",
task: "Parent task",
ts: 1000,
number: 1,
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
status: "delegated",
awaitingChildId: "child-1",
delegatedToId: "child-1",
childIds: ["child-1"],
},
})
await (ClineProvider.prototype as any).removeClineFromStack.call(provider)
// Stack should be empty after pop
expect(provider.clineStack).toHaveLength(0)
// Parent lookup should have been called
expect(getTaskWithId).toHaveBeenCalledWith("parent-1")
// Parent metadata should be repaired
expect(updateTaskHistory).toHaveBeenCalledTimes(1)
const updatedParent = updateTaskHistory.mock.calls[0][0]
expect(updatedParent).toEqual(
expect.objectContaining({
id: "parent-1",
status: "active",
awaitingChildId: undefined,
}),
)
// Log the repair
expect(provider.log).toHaveBeenCalledWith(expect.stringContaining("Repaired parent parent-1 metadata"))
})
it("does NOT modify parent metadata when the task has no parentTaskId (non-delegated)", async () => {
const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({
childTaskId: "standalone-1",
// No parentTaskId — this is a top-level task
})
await (ClineProvider.prototype as any).removeClineFromStack.call(provider)
// Stack should be empty
expect(provider.clineStack).toHaveLength(0)
// No parent lookup or update should happen
expect(getTaskWithId).not.toHaveBeenCalled()
expect(updateTaskHistory).not.toHaveBeenCalled()
})
it("does NOT modify parent metadata when awaitingChildId does not match the popped child", async () => {
const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({
childTaskId: "child-1",
parentTaskId: "parent-1",
parentHistoryItem: {
id: "parent-1",
task: "Parent task",
ts: 1000,
number: 1,
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
status: "delegated",
awaitingChildId: "child-OTHER", // different child
delegatedToId: "child-OTHER",
childIds: ["child-OTHER"],
},
})
await (ClineProvider.prototype as any).removeClineFromStack.call(provider)
// Parent was looked up but should NOT be updated
expect(getTaskWithId).toHaveBeenCalledWith("parent-1")
expect(updateTaskHistory).not.toHaveBeenCalled()
})
it("does NOT modify parent metadata when parent status is not 'delegated'", async () => {
const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({
childTaskId: "child-1",
parentTaskId: "parent-1",
parentHistoryItem: {
id: "parent-1",
task: "Parent task",
ts: 1000,
number: 1,
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
status: "completed", // already completed
awaitingChildId: "child-1",
childIds: ["child-1"],
},
})
await (ClineProvider.prototype as any).removeClineFromStack.call(provider)
expect(getTaskWithId).toHaveBeenCalledWith("parent-1")
expect(updateTaskHistory).not.toHaveBeenCalled()
})
it("catches and logs errors during parent metadata repair without blocking the pop", async () => {
const { provider, childTask, updateTaskHistory, getTaskWithId } = buildMockProvider({
childTaskId: "child-1",
parentTaskId: "parent-1",
getTaskWithIdError: new Error("Storage unavailable"),
})
// Should NOT throw
await (ClineProvider.prototype as any).removeClineFromStack.call(provider)
// Stack should still be empty (pop was not blocked)
expect(provider.clineStack).toHaveLength(0)
// The abort should still have been called
expect(childTask.abortTask).toHaveBeenCalledWith(true)
// Error should be logged as non-fatal
expect(provider.log).toHaveBeenCalledWith(
expect.stringContaining("Failed to repair parent metadata for parent-1 (non-fatal)"),
)
// No update should have been attempted
expect(updateTaskHistory).not.toHaveBeenCalled()
})
it("handles empty stack gracefully", async () => {
const provider = {
clineStack: [] as any[],
taskEventListeners: new Map(),
log: vi.fn(),
getTaskWithId: vi.fn(),
updateTaskHistory: vi.fn(),
}
// Should not throw
await (ClineProvider.prototype as any).removeClineFromStack.call(provider)
expect(provider.clineStack).toHaveLength(0)
expect(provider.getTaskWithId).not.toHaveBeenCalled()
expect(provider.updateTaskHistory).not.toHaveBeenCalled()
})
it("skips delegation repair when skipDelegationRepair option is true", async () => {
const { provider, updateTaskHistory, getTaskWithId } = buildMockProvider({
childTaskId: "child-1",
parentTaskId: "parent-1",
parentHistoryItem: {
id: "parent-1",
task: "Parent task",
ts: 1000,
number: 1,
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
status: "delegated",
awaitingChildId: "child-1",
delegatedToId: "child-1",
childIds: ["child-1"],
},
})
// Call with skipDelegationRepair: true (as delegateParentAndOpenChild would)
await (ClineProvider.prototype as any).removeClineFromStack.call(provider, { skipDelegationRepair: true })
// Stack should be empty after pop
expect(provider.clineStack).toHaveLength(0)
// Parent lookup should NOT have been called — repair was skipped entirely
expect(getTaskWithId).not.toHaveBeenCalled()
expect(updateTaskHistory).not.toHaveBeenCalled()
})
it("does NOT reset grandparent during A→B→C nested delegation transition", async () => {
// Scenario: A delegated to B, B is now delegating to C.
// delegateParentAndOpenChild() pops B via removeClineFromStack({ skipDelegationRepair: true }).
// Grandparent A should remain "delegated" — its metadata must not be repaired.
const grandparentHistory = {
id: "task-A",
task: "Grandparent task",
ts: 1000,
number: 1,
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
status: "delegated",
awaitingChildId: "task-B",
delegatedToId: "task-B",
childIds: ["task-B"],
}
const taskB = {
taskId: "task-B",
instanceId: "inst-B",
parentTaskId: "task-A",
emit: vi.fn(),
abortTask: vi.fn().mockResolvedValue(undefined),
}
const getTaskWithId = vi.fn().mockImplementation(async (id: string) => {
if (id === "task-A") {
return { historyItem: { ...grandparentHistory } }
}
throw new Error("Task not found")
})
const updateTaskHistory = vi.fn().mockResolvedValue([])
const provider = {
clineStack: [taskB] as any[],
taskEventListeners: new Map(),
log: vi.fn(),
getTaskWithId,
updateTaskHistory,
}
// Simulate what delegateParentAndOpenChild does: pop B with skipDelegationRepair
await (ClineProvider.prototype as any).removeClineFromStack.call(provider, { skipDelegationRepair: true })
// B was popped
expect(provider.clineStack).toHaveLength(0)
// Grandparent A should NOT have been looked up or modified
expect(getTaskWithId).not.toHaveBeenCalled()
expect(updateTaskHistory).not.toHaveBeenCalled()
// Grandparent A's metadata remains intact (delegated, awaitingChildId: task-B)
// The caller (delegateParentAndOpenChild) will update A to point to C separately.
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,331 +1,112 @@
import OpenAI from "openai"
import { Anthropic } from "@anthropic-ai/sdk" // Keep for type usage only
/**
* LiteLLM provider handler using Vercel AI SDK.
*
* This handler uses @ai-sdk/openai-compatible to communicate with LiteLLM proxy servers.
* LiteLLM follows the OpenAI API format, making it compatible with the OpenAI-compatible provider.
* Models are dynamically fetched from the LiteLLM server via /v1/model/info.
*/
import { litellmDefaultModelId, litellmDefaultModelInfo } from "@roo-code/types"
import { Anthropic } from "@anthropic-ai/sdk"
import { LanguageModel } from "ai"
import { calculateApiCostOpenAI } from "../../shared/cost"
import { litellmDefaultModelId, litellmDefaultModelInfo, type ModelInfo, type ModelRecord } from "@roo-code/types"
import { ApiHandlerOptions } from "../../shared/api"
import { type ApiHandlerOptions, getModelMaxOutputTokens } from "../../shared/api"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
import { OpenAICompatibleHandler } from "./openai-compatible"
import { getModels, getModelsFromCache } from "./fetchers/modelCache"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { RouterProvider } from "./router-provider"
/**
* LiteLLM provider handler
*
* This handler uses the LiteLLM API to proxy requests to various LLM providers.
* It follows the OpenAI API format for compatibility.
*/
export class LiteLLMHandler extends RouterProvider implements SingleCompletionHandler {
export class LiteLLMHandler extends OpenAICompatibleHandler implements SingleCompletionHandler {
private models: ModelRecord = {}
constructor(options: ApiHandlerOptions) {
super({
options,
name: "litellm",
baseURL: `${options.litellmBaseUrl || "http://localhost:4000"}`,
const modelId = options.litellmModelId || litellmDefaultModelId
super(options, {
providerName: "LiteLLM",
baseURL: options.litellmBaseUrl || "http://localhost:4000",
apiKey: options.litellmApiKey || "dummy-key",
modelId: options.litellmModelId,
defaultModelId: litellmDefaultModelId,
defaultModelInfo: litellmDefaultModelInfo,
modelId,
modelInfo: litellmDefaultModelInfo,
temperature: options.modelTemperature ?? 0,
modelMaxTokens: options.modelMaxTokens,
})
}
private isGpt5(modelId: string): boolean {
// Match gpt-5, gpt5, and variants like gpt-5o, gpt-5-turbo, gpt5-preview, gpt-5.1
// Avoid matching gpt-50, gpt-500, etc.
return /\bgpt-?5(?!\d)/i.test(modelId)
private async fetchModel() {
this.models = await getModels({
provider: "litellm",
apiKey: this.config.apiKey,
baseUrl: this.config.baseURL,
})
return this.getModel()
}
/**
* Detect if the model is a Gemini model that requires thought signature handling.
* Gemini 3 models validate thought signatures for tool/function calling steps.
*/
private isGeminiModel(modelId: string): boolean {
// Match various Gemini model patterns:
// - gemini-3-pro, gemini-3-flash, gemini-3-*
// - gemini 3 pro, Gemini 3 Pro (space-separated, case-insensitive)
// - gemini/gemini-3-*, google/gemini-3-*
// - vertex_ai/gemini-3-*, vertex/gemini-3-*
// Also match Gemini 2.5+ models which use similar validation
const lowerModelId = modelId.toLowerCase()
override getModel(): { id: string; info: ModelInfo } {
const id = this.config.modelId || litellmDefaultModelId
if (this.models[id]) {
return { id, info: this.models[id] }
}
const cachedModels = getModelsFromCache("litellm")
if (cachedModels?.[id]) {
this.models = cachedModels
return { id, info: cachedModels[id] }
}
return { id: this.config.modelId || litellmDefaultModelId, info: litellmDefaultModelInfo }
}
protected override getLanguageModel(): LanguageModel {
const { id } = this.getModel()
return this.provider(id)
}
protected override getMaxOutputTokens(): number | undefined {
const { id, info } = this.getModel()
return (
// Match hyphenated versions: gemini-3, gemini-2.5
lowerModelId.includes("gemini-3") ||
lowerModelId.includes("gemini-2.5") ||
// Match space-separated versions: "gemini 3", "gemini 2.5"
// This handles model names like "Gemini 3 Pro" from LiteLLM model groups
lowerModelId.includes("gemini 3") ||
lowerModelId.includes("gemini 2.5") ||
// Also match provider-prefixed versions
/\b(gemini|google|vertex_ai|vertex)\/gemini[-\s](3|2\.5)/i.test(modelId)
getModelMaxOutputTokens({
modelId: id,
model: info,
settings: this.options,
format: "openai",
}) ?? undefined
)
}
/**
* Inject thought signatures for Gemini models via provider_specific_fields.
* This is required when switching from other models to Gemini to satisfy API validation
* for function calls that weren't generated by Gemini (and thus lack thought signatures).
*
* Per LiteLLM documentation:
* - Thought signatures are stored in provider_specific_fields.thought_signature of tool calls
* - The dummy signature base64("skip_thought_signature_validator") bypasses validation
*
* We inject the dummy signature on EVERY tool call unconditionally to ensure Gemini
* doesn't complain about missing/corrupted signatures when conversation history
* contains tool calls from other models (like Claude).
*/
private injectThoughtSignatureForGemini(
openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[],
): OpenAI.Chat.ChatCompletionMessageParam[] {
// Base64 encoded "skip_thought_signature_validator" as per LiteLLM docs
const dummySignature = Buffer.from("skip_thought_signature_validator").toString("base64")
return openAiMessages.map((msg) => {
if (msg.role === "assistant") {
const toolCalls = (msg as any).tool_calls as any[] | undefined
// Only process if there are tool calls
if (toolCalls && toolCalls.length > 0) {
// Inject dummy signature into ALL tool calls' provider_specific_fields
// This ensures Gemini doesn't reject tool calls from other models
const updatedToolCalls = toolCalls.map((tc) => ({
...tc,
provider_specific_fields: {
...(tc.provider_specific_fields || {}),
thought_signature: dummySignature,
},
}))
return {
...msg,
tool_calls: updatedToolCalls,
}
}
}
return msg
})
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const { id: modelId, info } = await this.fetchModel()
const openAiMessages = convertToOpenAiMessages(messages, {
normalizeToolCallId: sanitizeOpenAiCallId,
})
// Prepare messages with cache control if enabled and supported
let systemMessage: OpenAI.Chat.ChatCompletionMessageParam
let enhancedMessages: OpenAI.Chat.ChatCompletionMessageParam[]
if (this.options.litellmUsePromptCache && info.supportsPromptCache) {
// Create system message with cache control in the proper format
systemMessage = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
cache_control: { type: "ephemeral" },
} as any,
],
}
// Find the last two user messages to apply caching
const userMsgIndices = openAiMessages.reduce(
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
[] as number[],
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
// Apply cache_control to the last two user messages
enhancedMessages = openAiMessages.map((message, index) => {
if ((index === lastUserMsgIndex || index === secondLastUserMsgIndex) && message.role === "user") {
// Handle both string and array content types
if (typeof message.content === "string") {
return {
...message,
content: [
{
type: "text",
text: message.content,
cache_control: { type: "ephemeral" },
} as any,
],
}
} else if (Array.isArray(message.content)) {
// Apply cache control to the last content item in the array
return {
...message,
content: message.content.map((content, contentIndex) =>
contentIndex === message.content.length - 1
? ({
...content,
cache_control: { type: "ephemeral" },
} as any)
: content,
),
}
}
}
return message
})
} else {
// No cache control - use simple format
systemMessage = { role: "system", content: systemPrompt }
enhancedMessages = openAiMessages
}
// Required by some providers; others default to max tokens allowed
let maxTokens: number | undefined = info.maxTokens ?? undefined
// Check if this is a GPT-5 model that requires max_completion_tokens instead of max_tokens
const isGPT5Model = this.isGpt5(modelId)
// For Gemini models with native protocol: inject fake reasoning.encrypted block for tool calls
// This is required when switching from other models to Gemini to satisfy API validation.
// Gemini 3 models validate thought signatures for function calls, and when conversation
// history contains tool calls from other models (like Claude), they lack the required
// signatures. The "skip_thought_signature_validator" value bypasses this validation.
const isGemini = this.isGeminiModel(modelId)
let processedMessages = enhancedMessages
if (isGemini) {
processedMessages = this.injectThoughtSignatureForGemini(enhancedMessages)
}
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelId,
messages: [systemMessage, ...processedMessages],
stream: true,
stream_options: {
include_usage: true,
},
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
}
// GPT-5 models require max_completion_tokens instead of the deprecated max_tokens parameter
if (isGPT5Model && maxTokens) {
requestOptions.max_completion_tokens = maxTokens
} else if (maxTokens) {
requestOptions.max_tokens = maxTokens
}
if (this.supportsTemperature(modelId)) {
requestOptions.temperature = this.options.modelTemperature ?? 0
}
try {
const { data: completion } = await this.client.chat.completions.create(requestOptions).withResponse()
let lastUsage
for await (const chunk of completion) {
const delta = chunk.choices[0]?.delta
const usage = chunk.usage as LiteLLMUsage
if (delta?.content) {
yield { type: "text", text: delta.content }
}
// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
yield {
type: "tool_call_partial",
index: toolCall.index,
id: toolCall.id,
name: toolCall.function?.name,
arguments: toolCall.function?.arguments,
}
}
}
if (usage) {
lastUsage = usage
}
}
if (lastUsage) {
// Extract cache-related information if available
// LiteLLM may use different field names for cache tokens
const cacheWriteTokens =
lastUsage.cache_creation_input_tokens || (lastUsage as any).prompt_cache_miss_tokens || 0
const cacheReadTokens =
lastUsage.prompt_tokens_details?.cached_tokens ||
(lastUsage as any).cache_read_input_tokens ||
(lastUsage as any).prompt_cache_hit_tokens ||
0
const { totalCost } = calculateApiCostOpenAI(
info,
lastUsage.prompt_tokens || 0,
lastUsage.completion_tokens || 0,
cacheWriteTokens,
cacheReadTokens,
)
const usageData: ApiStreamUsageChunk = {
type: "usage",
inputTokens: lastUsage.prompt_tokens || 0,
outputTokens: lastUsage.completion_tokens || 0,
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
totalCost,
}
yield usageData
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`LiteLLM streaming error: ${error.message}`)
}
throw error
}
await this.fetchModel()
yield* super.createMessage(systemPrompt, messages, metadata)
}
async completePrompt(prompt: string): Promise<string> {
const { id: modelId, info } = await this.fetchModel()
override async completePrompt(prompt: string): Promise<string> {
await this.fetchModel()
return super.completePrompt(prompt)
}
// Check if this is a GPT-5 model that requires max_completion_tokens instead of max_tokens
const isGPT5Model = this.isGpt5(modelId)
try {
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
model: modelId,
messages: [{ role: "user", content: prompt }],
}
if (this.supportsTemperature(modelId)) {
requestOptions.temperature = this.options.modelTemperature ?? 0
}
// GPT-5 models require max_completion_tokens instead of the deprecated max_tokens parameter
if (isGPT5Model && info.maxTokens) {
requestOptions.max_completion_tokens = info.maxTokens
} else if (info.maxTokens) {
requestOptions.max_tokens = info.maxTokens
}
const response = await this.client.chat.completions.create(requestOptions)
return response.choices[0]?.message.content || ""
} catch (error) {
if (error instanceof Error) {
throw new Error(`LiteLLM completion error: ${error.message}`)
}
throw error
protected override processUsageMetrics(usage: {
inputTokens?: number
outputTokens?: number
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
raw?: Record<string, unknown>
}): ApiStreamUsageChunk {
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
cacheReadTokens: usage.details?.cachedInputTokens,
reasoningTokens: usage.details?.reasoningTokens,
}
}
}
// LiteLLM usage may include an extra field for Anthropic use cases.
interface LiteLLMUsage extends OpenAI.CompletionUsage {
cache_creation_input_tokens?: number
}

View file

@ -456,7 +456,7 @@ export class ClineProvider
// Removes and destroys the top Cline instance (the current finished task),
// activating the previous one (resuming the parent task).
async removeClineFromStack() {
async removeClineFromStack(options?: { skipDelegationRepair?: boolean }) {
if (this.clineStack.length === 0) {
return
}
@ -465,6 +465,11 @@ export class ClineProvider
let task = this.clineStack.pop()
if (task) {
// Capture delegation metadata before abort/dispose, since abortTask(true)
// is async and the task reference is cleared afterwards.
const childTaskId = task.taskId
const parentTaskId = task.parentTaskId
task.emit(RooCodeEventName.TaskUnfocused)
try {
@ -488,6 +493,37 @@ export class ClineProvider
// Make sure no reference kept, once promises end it will be
// garbage collected.
task = undefined
// Delegation-aware parent metadata repair:
// If the popped task was a delegated child, repair the parent's metadata
// so it transitions from "delegated" back to "active" and becomes resumable
// from the task history list.
// Skip when called from delegateParentAndOpenChild() during nested delegation
// transitions (A→B→C), where the caller intentionally replaces the active
// child and will update the parent to point at the new child.
if (parentTaskId && childTaskId && !options?.skipDelegationRepair) {
try {
const { historyItem: parentHistory } = await this.getTaskWithId(parentTaskId)
if (parentHistory.status === "delegated" && parentHistory.awaitingChildId === childTaskId) {
await this.updateTaskHistory({
...parentHistory,
status: "active",
awaitingChildId: undefined,
})
this.log(
`[ClineProvider#removeClineFromStack] Repaired parent ${parentTaskId} metadata: delegated → active (child ${childTaskId} removed)`,
)
}
} catch (err) {
// Non-fatal: log but do not block the pop operation.
this.log(
`[ClineProvider#removeClineFromStack] Failed to repair parent metadata for ${parentTaskId} (non-fatal): ${
err instanceof Error ? err.message : String(err)
}`,
)
}
}
}
}
@ -2080,7 +2116,6 @@ export class ClineProvider
historyPreviewCollapsed,
reasoningBlockCollapsed,
enterBehavior,
taskHeaderHighlightEnabled,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
@ -2226,7 +2261,6 @@ 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,
@ -3270,7 +3304,7 @@ export class ClineProvider
// This ensures we never have >1 tasks open at any time during delegation.
// Await abort completion to ensure clean disposal and prevent unhandled rejections.
try {
await this.removeClineFromStack()
await this.removeClineFromStack({ skipDelegationRepair: true })
} catch (error) {
this.log(
`[delegateParentAndOpenChild] Error during parent disposal (non-fatal): ${

View file

@ -68,8 +68,7 @@ const TaskHeader = ({
todos,
}: TaskHeaderProps) => {
const { t } = useTranslation()
const { apiConfiguration, currentTaskItem, clineMessages, isBrowserSessionActive, taskHeaderHighlightEnabled } =
useExtensionState()
const { apiConfiguration, currentTaskItem, clineMessages, isBrowserSessionActive } = useExtensionState()
const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false)
@ -77,34 +76,19 @@ 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 = 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])
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
useEffect(() => {
const timer = setTimeout(() => {
@ -157,6 +141,9 @@ 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 })
@ -187,14 +174,12 @@ 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

View file

@ -40,7 +40,6 @@ let mockExtensionState: {
apiConfiguration: ProviderSettings
currentTaskItem: { id: string } | null
clineMessages: any[]
taskHeaderHighlightEnabled?: boolean
} = {
apiConfiguration: {
apiProvider: "anthropic",
@ -49,7 +48,6 @@ let mockExtensionState: {
} as ProviderSettings,
currentTaskItem: { id: "test-task-id" },
clineMessages: [],
taskHeaderHighlightEnabled: false,
}
// Mock the ExtensionStateContext
@ -217,7 +215,6 @@ describe("TaskHeader", () => {
} as ProviderSettings,
currentTaskItem: { id: "test-task-id" },
clineMessages: [],
taskHeaderHighlightEnabled: false,
}
})
@ -426,175 +423,6 @@ 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

View file

@ -124,11 +124,6 @@ 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()
@ -152,7 +147,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
const prevApiConfigName = useRef(currentApiConfigName)
const confirmDialogHandler = useRef<() => void>()
const [cachedState, setCachedState] = useState(() => withCachedStateDefaults(extensionState))
const [cachedState, setCachedState] = useState(() => extensionState)
const {
alwaysAllowReadOnly,
@ -230,7 +225,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
return
}
setCachedState((prevCachedState) => withCachedStateDefaults({ ...prevCachedState, ...extensionState }))
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
prevApiConfigName.current = currentApiConfigName
setChangeDetected(false)
}, [currentApiConfigName, extensionState])
@ -238,7 +233,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
// Bust the cache when settings are imported.
useEffect(() => {
if (settingsImportedAt) {
setCachedState((prevCachedState) => withCachedStateDefaults({ ...prevCachedState, ...extensionState }))
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
setChangeDetected(false)
}
}, [settingsImportedAt, extensionState])
@ -435,7 +430,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
enterBehavior: enterBehavior ?? "send",
taskHeaderHighlightEnabled,
includeCurrentTime: includeCurrentTime ?? true,
includeCurrentCost: includeCurrentCost ?? true,
showQuestionsOneByOne: showQuestionsOneByOne ?? false,
@ -477,7 +471,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
(confirm: boolean) => {
if (confirm) {
// Discard changes: Reset state and flag
setCachedState(withCachedStateDefaults(extensionState)) // Revert to original state
setCachedState(extensionState) // Revert to original state
setChangeDetected(false) // Reset change flag
confirmDialogHandler.current?.() // Execute the pending action (e.g., tab switch)
}
@ -928,10 +922,10 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
{/* UI Section */}
{renderTab === "ui" && (
<UISettings
taskHeaderHighlightEnabled={taskHeaderHighlightEnabled ?? false}
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
enterBehavior={enterBehavior ?? "send"}
showQuestionsOneByOne={showQuestionsOneByOne ?? false}
taskHeaderHighlightEnabled={taskHeaderHighlightEnabled ?? false}
setCachedStateField={setCachedStateField}
/>
)}

View file

@ -71,26 +71,6 @@ export const UISettings = ({
<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"
@ -99,9 +79,7 @@ export const UISettings = ({
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={reasoningBlockCollapsed}
onChange={(event) =>
handleReasoningBlockCollapsedChange((event.target as HTMLInputElement).checked)
}
onChange={(e: any) => handleReasoningBlockCollapsedChange(e.target.checked)}
data-testid="collapse-thinking-checkbox">
<span className="font-medium">{t("settings:ui.collapseThinking.label")}</span>
</VSCodeCheckbox>
@ -119,9 +97,7 @@ export const UISettings = ({
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={enterBehavior === "newline"}
onChange={(event) =>
handleEnterBehaviorChange((event.target as HTMLInputElement).checked)
}
onChange={(e: any) => handleEnterBehaviorChange(e.target.checked)}
data-testid="enter-behavior-checkbox">
<span className="font-medium">
{t("settings:ui.requireCtrlEnterToSend.label", { primaryMod })}
@ -150,6 +126,24 @@ export const UISettings = ({
</div>
</div>
</SearchableSetting>
{/* Task Header Highlight Setting */}
<SearchableSetting
settingId="ui-task-header-highlight"
section="ui"
label={t("settings:ui.taskHeaderHighlight.label")}>
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={taskHeaderHighlightEnabled}
onChange={(e: any) => handleTaskHeaderHighlightChange(e.target.checked)}
data-testid="task-header-highlight-checkbox">
<span className="font-medium">{t("settings:ui.taskHeaderHighlight.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.taskHeaderHighlight.description")}
</div>
</div>
</SearchableSetting>
</div>
</Section>
</div>

View file

@ -2,33 +2,6 @@ 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,
@ -70,55 +43,4 @@ 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)
})
})
})
})

View file

@ -139,7 +139,6 @@ 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
@ -241,7 +240,6 @@ 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: [],

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

@ -157,10 +157,6 @@
"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"

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

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

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

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

View file

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

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

@ -1018,10 +1018,6 @@
}
},
"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ę"

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

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

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

@ -1018,10 +1018,6 @@
}
},
"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"

View file

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

View file

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

View file

@ -590,30 +590,3 @@ 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;
}