mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
chore: merge origin/main into vk/6325-multi-question and resolve conflicts
This commit is contained in:
commit
c947059979
31 changed files with 870 additions and 1531 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -285,7 +285,6 @@ export type ExtensionState = Pick<
|
|||
| "ttsSpeed"
|
||||
| "soundEnabled"
|
||||
| "soundVolume"
|
||||
| "taskHeaderHighlightEnabled"
|
||||
| "terminalOutputPreviewSize"
|
||||
| "terminalShellIntegrationTimeout"
|
||||
| "terminalShellIntegrationDisabled"
|
||||
|
|
|
|||
281
src/__tests__/removeClineFromStack-delegation.spec.ts
Normal file
281
src/__tests__/removeClineFromStack-delegation.spec.ts
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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): ${
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ca/settings.json
generated
4
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/de/settings.json
generated
4
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/es/settings.json
generated
4
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/fr/settings.json
generated
4
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/hi/settings.json
generated
4
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -1018,10 +1018,6 @@
|
|||
}
|
||||
},
|
||||
"ui": {
|
||||
"taskCompleteColor": {
|
||||
"label": "टास्क हेडर हाइलाइट",
|
||||
"description": "जब टास्क पूरे होते हैं या तुम्हारे ध्यान की ज़रूरत होती है, तो टास्क हेडर को रंग संकेतक से हाइलाइट करता है, ताकि तुम जल्दी से पहचान सको कि कब कार्रवाई ज़रूरी है"
|
||||
},
|
||||
"collapseThinking": {
|
||||
"label": "सोच संदेशों को डिफ़ॉल्ट रूप से संक्षिप्त करें",
|
||||
"description": "सक्षम होने पर, सोच ब्लॉक आपके द्वारा उनके साथ इंटरैक्ट करने तक डिफ़ॉल्ट रूप से संक्षिप्त रहेंगे"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/id/settings.json
generated
4
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/it/settings.json
generated
4
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ja/settings.json
generated
4
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -1018,10 +1018,6 @@
|
|||
}
|
||||
},
|
||||
"ui": {
|
||||
"taskCompleteColor": {
|
||||
"label": "タスクヘッダーのハイライト",
|
||||
"description": "タスクが完了したり注意が必要なときに、タスクヘッダーをカラーインジケーターでハイライトし、アクションが必要なタイミングをすばやく把握できるようにします"
|
||||
},
|
||||
"collapseThinking": {
|
||||
"label": "デフォルトで思考メッセージを折りたたむ",
|
||||
"description": "有効にすると、操作するまで思考ブロックがデフォルトで折りたたまれます"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ko/settings.json
generated
4
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -1018,10 +1018,6 @@
|
|||
}
|
||||
},
|
||||
"ui": {
|
||||
"taskCompleteColor": {
|
||||
"label": "작업 헤더 하이라이트",
|
||||
"description": "작업이 완료되거나 주의가 필요할 때 작업 헤더를 색상 표시로 강조하여 조치가 필요한 시점을 빠르게 파악할 수 있도록 합니다"
|
||||
},
|
||||
"collapseThinking": {
|
||||
"label": "기본적으로 생각 메시지 접기",
|
||||
"description": "활성화하면 상호 작용할 때까지 생각 블록이 기본적으로 접힙니다"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/nl/settings.json
generated
4
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pl/settings.json
generated
4
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -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ę"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
4
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ru/settings.json
generated
4
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -1018,10 +1018,6 @@
|
|||
}
|
||||
},
|
||||
"ui": {
|
||||
"taskCompleteColor": {
|
||||
"label": "Подсветка заголовка задачи",
|
||||
"description": "Подсвечивает заголовок задачи цветовым индикатором при завершении задач или когда требуется твоё внимание, помогая быстро заметить, когда необходимо действие"
|
||||
},
|
||||
"collapseThinking": {
|
||||
"label": "Сворачивать сообщения о размышлениях по умолчанию",
|
||||
"description": "Если включено, блоки с размышлениями будут свернуты по умолчанию, пока вы не начнете с ними взаимодействовать"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/tr/settings.json
generated
4
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/vi/settings.json
generated
4
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
4
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -1018,10 +1018,6 @@
|
|||
}
|
||||
},
|
||||
"ui": {
|
||||
"taskCompleteColor": {
|
||||
"label": "任务标题高亮",
|
||||
"description": "当任务完成或需要你关注时,通过颜色指示器高亮任务标题,帮助你快速发现需要操作的时机"
|
||||
},
|
||||
"collapseThinking": {
|
||||
"label": "默认折叠「思考」消息",
|
||||
"description": "启用后,「思考」块将默认折叠,直到您与其交互"
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
4
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -104,10 +104,6 @@
|
|||
"footer": "使用斜線命令快速存取經常使用的提示詞和工作流程。"
|
||||
},
|
||||
"ui": {
|
||||
"taskCompleteColor": {
|
||||
"label": "工作標題醒目提示",
|
||||
"description": "當工作完成或需要你注意時,以顏色指示器醒目提示工作標題,幫助你快速發現需要採取行動的時機"
|
||||
},
|
||||
"collapseThinking": {
|
||||
"label": "預設折疊「思考」訊息",
|
||||
"description": "啟用後,「思考」塊將預設折疊,直到您與其互動"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue