Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6ea45b3
feat(task): task-local thinking effort state, per-request override, a…
easonliang28 Aug 21, 2026
9275aa1
Merge remote-tracking branch 'upstream/main' into feat/dte-2-task-state
easonliang28 Aug 22, 2026
14d1f35
fix(task): keep override restore value current across profile switches
easonliang28 Aug 22, 2026
90b47b0
docs(task): JSDoc for diff-touched functions flagged by CodeRabbit
easonliang28 Aug 22, 2026
cce1aea
Merge remote-tracking branch 'upstream/main' into feat/dte-5-orchestr…
easonliang28 Aug 22, 2026
146c5c8
feat(task): orchestrator new_task thinking_effort
easonliang28 Aug 23, 2026
f6410bb
Merge remote-tracking branch 'upstream/main' into feat/dte-5-orchestr…
easonliang28 Aug 23, 2026
4eb13a9
fix(task): new_task thinking_effort review fixes (boolean capability,…
easonliang28 Aug 23, 2026
4a6ee69
fix(task): forward new_task thinking_effort through native tool-call …
easonliang28 Aug 23, 2026
018f165
fix(task): keep new_task delegation alive when the fallback effort sa…
easonliang28 Aug 23, 2026
026bca7
test(task): document partial provider double and assert delegation me…
easonliang28 Aug 23, 2026
3ea9f63
webview(chat): localize new_task thinking effort selector labels
easonliang28 Aug 23, 2026
4b655b4
webview(chat): restyle new_task effort selector with dropdown tokens …
easonliang28 Aug 25, 2026
7903293
chore: re-run CI after flaky e2e-mock failure
easonliang28 Aug 25, 2026
fea97fd
webview(chat): keep native focus outline on new_task effort selector
easonliang28 Aug 25, 2026
0f2c282
test(dte-5): cover the remaining partial branches to reach 100% patch…
easonliang28 Aug 25, 2026
f1e8667
test(webview): document partial ClineProvider double in provider-dele…
easonliang28 Aug 25, 2026
2ab330e
fix(tools): do not name a level in the empty-capability new_task hint
easonliang28 Aug 26, 2026
6eba686
fix(dte-5): normalize new_task capability levels before use
easonliang28 Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { CloudUserInfo, CloudOrganizationMembership, OrganizationAllowList,
import type { SerializedCustomToolDefinition } from "./custom-tool.js"
import type { GitCommit } from "./git.js"
import type { McpServer } from "./mcp.js"
import { RouterModelsMessageType, type ModelRecord, type RouterModels } from "./model.js"
import { RouterModelsMessageType, type ModelRecord, type RouterModels, type ReasoningEffortExtended } from "./model.js"
import { LmStudioModelsMessageType } from "./providers/lm-studio.js"
import { OllamaModelsMessageType } from "./providers/ollama.js"
import { OpenAiModelsMessageType } from "./providers/openai.js"
Expand Down Expand Up @@ -648,6 +648,9 @@ export interface WebviewMessage {
| "themeFixtureProbeResponse"
text?: string
taskId?: string
// DTE series 5/5: thinking effort chosen in the pending new_task ask block
// (sent with the ask response, see Task.handleWebviewAskResponse).
thinkingEffort?: ReasoningEffortExtended
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
disabled?: boolean
Expand Down Expand Up @@ -904,6 +907,10 @@ export interface ClineSayTool {
description?: string
// Properties for skill tool
skill?: string
// DTE series 5/5: new_task thinking-effort prefill for the ask block and the
// effort levels the target model supports (see NewTaskTool).
thinkingEffort?: ReasoningEffortExtended
supportedThinkingEfforts?: ReasoningEffortExtended[]
}

export interface ClineAskUseMcpServer {
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/new-task-delegation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@ describe("Task.startSubtask() metadata-driven delegation", () => {
;(parent as any).taskId = "parent-1"
;(parent as any).providerRef = { deref: () => provider }
;(parent as any).emit = vi.fn()
// DTE series 5/5: startSubtask now passes the parent's effective effort to the
// child's init; this Object.create double bypasses the constructor, so shadow
// the public resolver with the value under test.
parent.resolveNewTaskEffectiveEffort = () => undefined

const child = await (Task.prototype as any).startSubtask.call(parent, "Do something", [], "code")

// DTE series 5/5: thinkingEffort is always present (undefined = inherit parent effective).
expect(provider.delegateParentAndOpenChild).toHaveBeenCalledWith({
parentTaskId: "parent-1",
message: "Do something",
initialTodos: [],
mode: "code",
thinkingEffort: undefined,
})
expect(child.taskId).toBe("child-1")

Expand Down
236 changes: 236 additions & 0 deletions src/__tests__/provider-delegation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,240 @@ describe("ClineProvider.delegateParentAndOpenChild()", () => {
expect(deleteTaskWithId).toHaveBeenCalledWith("child-1", false)
expect(createTaskWithHistoryItem).toHaveBeenCalledWith(parentHistoryItem)
})

it("applies the parent-supplied starting effort to the child at init (DTE series 5/5)", async () => {
const parentTask = makeParentTask()
const setRuntimeThinkingEffort = vi.fn()
const childRun = vi.fn().mockResolvedValue(undefined)
const createTask = vi.fn().mockResolvedValue({
taskId: "child-1",
start: vi.fn(),
run: childRun,
setRuntimeThinkingEffort,
// The child's resolved model (post mode switch) supports the requested level.
api: {
getModel: () => ({ id: "child-model", info: { supportsReasoningEffort: ["low", "medium", "high"] } }),
},
})
const taskHistoryStore = makeStoreStub()

const provider = {
taskScheduler: new TaskScheduler(),
emit: vi.fn(),
getCurrentTask: vi.fn(() => parentTask),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
createTask,
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
log: vi.fn(),
isViewLaunched: false,
recentTasksCache: undefined,
taskHistoryStore,
} as unknown as ClineProvider

const child = await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, {
parentTaskId: "parent-1",
message: "Do something",
initialTodos: [],
mode: "code",
thinkingEffort: "high",
})
await Promise.resolve() // drain scheduler microtask so child.run() is invoked

expect(child.taskId).toBe("child-1")
// Applied as a task-local override with provenance "parent" before the child's
// first request (the child header shows it from the start).
expect(setRuntimeThinkingEffort).toHaveBeenCalledTimes(1)
expect(setRuntimeThinkingEffort).toHaveBeenCalledWith("high", "parent")
})

it("leaves the child's effort untouched when no starting effort is supplied (DTE series 5/5)", async () => {
const parentTask = makeParentTask()
const setRuntimeThinkingEffort = vi.fn()
const childRun = vi.fn().mockResolvedValue(undefined)
const createTask = vi.fn().mockResolvedValue({
taskId: "child-1",
start: vi.fn(),
run: childRun,
setRuntimeThinkingEffort,
})
const taskHistoryStore = makeStoreStub()

const provider = {
taskScheduler: new TaskScheduler(),
emit: vi.fn(),
getCurrentTask: vi.fn(() => parentTask),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
createTask,
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
log: vi.fn(),
isViewLaunched: false,
recentTasksCache: undefined,
taskHistoryStore,
} as unknown as ClineProvider

await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, {
parentTaskId: "parent-1",
message: "Do something",
initialTodos: [],
mode: "code",
})
await Promise.resolve()

expect(setRuntimeThinkingEffort).not.toHaveBeenCalled()
})

it("falls back with an observable say when the child model (post mode switch) does not support the effort (DTE series 5/5)", async () => {
const parentTask = makeParentTask()
const setRuntimeThinkingEffort = vi.fn()
const say = vi.fn().mockResolvedValue(undefined)
const childRun = vi.fn().mockResolvedValue(undefined)
// The mode switch resolved a DIFFERENT model than the parent's: it only
// supports low/high, so the parent-validated "xhigh" must not be applied.
const createTask = vi.fn().mockResolvedValue({
taskId: "child-1",
start: vi.fn(),
run: childRun,
setRuntimeThinkingEffort,
say,
api: { getModel: () => ({ id: "child-model", info: { supportsReasoningEffort: ["low", "high"] } }) },
})
const taskHistoryStore = makeStoreStub()

const provider = {
taskScheduler: new TaskScheduler(),
emit: vi.fn(),
getCurrentTask: vi.fn(() => parentTask),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
createTask,
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
log: vi.fn(),
isViewLaunched: false,
recentTasksCache: undefined,
taskHistoryStore,
} as unknown as ClineProvider

await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, {
parentTaskId: "parent-1",
message: "Do something",
initialTodos: [],
mode: "code",
thinkingEffort: "xhigh",
})
await Promise.resolve()

// No task-local override: the child runs with the settings-derived effort.
expect(setRuntimeThinkingEffort).not.toHaveBeenCalled()
// Observable on the child task: the fallback is announced, not silent.
expect(say).toHaveBeenCalledTimes(1)
const [sayType, sayText] = say.mock.calls[0]
expect(sayType).toBe("error")
expect(sayText).toContain("xhigh")
expect(sayText).toContain("child-model")
// Delegation itself still proceeds: the child runs.
expect(childRun).toHaveBeenCalledTimes(1)
})

it("does not abort delegation when the fallback say rejects after the parent is disposed (DTE series 5/5)", async () => {
const parentTask = makeParentTask()
const setRuntimeThinkingEffort = vi.fn()
// The parent is already disposed when this say runs, so the webview state can be
// gone and the say rejects (e.g. posting to a removed task).
const say = vi.fn().mockRejectedValue(new Error("task disposed"))
const childRun = vi.fn().mockResolvedValue(undefined)
const createTask = vi.fn().mockResolvedValue({
taskId: "child-1",
start: vi.fn(),
run: childRun,
setRuntimeThinkingEffort,
say,
api: { getModel: () => ({ id: "child-model", info: { supportsReasoningEffort: ["low", "high"] } }) },
})
const taskHistoryStore = makeStoreStub()
const providerLog = vi.fn()

// Partial provider double: the real ClineProvider.prototype.delegateParentAndOpenChild
// is invoked below via .call() with only the members that method reads, so the full
// interface is not implemented and the double assertion is the last-resort hand-off.
const provider = {
taskScheduler: new TaskScheduler(),
emit: vi.fn(),
getCurrentTask: vi.fn(() => parentTask),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
createTask,
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
log: providerLog,
isViewLaunched: false,
recentTasksCache: undefined,
taskHistoryStore,
} as unknown as ClineProvider
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Must NOT reject: the failing notification is non-fatal.
await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, {
parentTaskId: "parent-1",
message: "Do something",
initialTodos: [],
mode: "code",
thinkingEffort: "xhigh",
})
await Promise.resolve()

// The say rejection is surfaced through the provider log, not thrown.
expect(providerLog).toHaveBeenCalledWith(expect.stringContaining("non-fatal"))
expect(providerLog).toHaveBeenCalledWith(expect.stringContaining("task disposed"))
// Delegation metadata is still persisted for the (already disposed) parent:
// capture the updater's resulting item and assert the delegated status and both
// child links, not just that the metadata transaction was entered.
expect(taskHistoryStore.atomicReadAndUpdate).toHaveBeenCalledTimes(1)
const [calledTaskId, updater] = taskHistoryStore.atomicReadAndUpdate.mock.calls[0]
expect(calledTaskId).toBe("parent-1")
expect(updater(parentHistoryItem)).toMatchObject({
id: "parent-1",
status: "delegated",
delegatedToId: "child-1",
awaitingChildId: "child-1",
childIds: expect.arrayContaining(["child-1"]),
})
// And the child is still scheduled despite the failed notification.
expect(childRun).toHaveBeenCalledTimes(1)
})

it("applies the effort when the child model (post mode switch) has a boolean-true capability (DTE series 5/5)", async () => {
const parentTask = makeParentTask()
const setRuntimeThinkingEffort = vi.fn()
const childRun = vi.fn().mockResolvedValue(undefined)
// Boolean-true capability: the child model supports every level.
const createTask = vi.fn().mockResolvedValue({
taskId: "child-1",
start: vi.fn(),
run: childRun,
setRuntimeThinkingEffort,
api: { getModel: () => ({ id: "child-model", info: { supportsReasoningEffort: true } }) },
})
const taskHistoryStore = makeStoreStub()

const provider = {
taskScheduler: new TaskScheduler(),
emit: vi.fn(),
getCurrentTask: vi.fn(() => parentTask),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
createTask,
handleModeSwitch: vi.fn().mockResolvedValue(undefined),
log: vi.fn(),
isViewLaunched: false,
recentTasksCache: undefined,
taskHistoryStore,
} as unknown as ClineProvider

await ClineProvider.prototype.delegateParentAndOpenChild.call(provider, {
parentTaskId: "parent-1",
message: "Do something",
initialTodos: [],
mode: "code",
thinkingEffort: "xhigh",
})
await Promise.resolve()

expect(setRuntimeThinkingEffort).toHaveBeenCalledTimes(1)
expect(setRuntimeThinkingEffort).toHaveBeenCalledWith("xhigh", "parent")
})
})
9 changes: 9 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
retiredProviderIdentifiers,
type ProviderSettings,
type ModelInfo,
type ReasoningEffortExtended,
} from "@roo-code/types"

import { getRouterRemovalMessage } from "../core/config/routerRemoval"
Expand Down Expand Up @@ -115,6 +116,14 @@ export interface ApiHandlerCreateMessageMetadata {
* when the user clicks stop, preventing wasted API tokens/compute on the provider side.
*/
abortSignal?: AbortSignal
/**
* Per-request thinking effort override (DTE series 2/5).
* When defined, takes precedence over the settings-derived `reasoningEffort`
* wherever the effective effort is resolved (see `resolveEffectiveReasoningEffort`).
* Task-scoped and transient: it applies to this request only (the next request
* after being set — no mid-stream effect) and is never persisted to settings.
*/
reasoningEffort?: ReasoningEffortExtended
}

export interface ApiHandler {
Expand Down
Loading
Loading