Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
19788c0
feat(settings): dynamic thinking effort experimental toggle (DTE-1)
easonliang28 Sep 4, 2026
82358bd
feat(api): per-request thinking effort override and adaptive effort e…
easonliang28 Sep 4, 2026
710bf61
feat(settings): dynamic thinking effort experimental toggle (DTE-1)
easonliang28 Sep 4, 2026
521e0e6
feat(settings): dynamic thinking effort experimental toggle (DTE-1)
easonliang28 Sep 4, 2026
e2f249a
Merge branch 'main' into feat/dte-v2-1-dynamic-thinking-effort
easonLiangWorldedtech Sep 4, 2026
53f22dc
Merge upstream main (0d937c05081) into feat/dte-v2-1-dynamic-thinking…
easonliang28 Sep 4, 2026
18f488f
Merge feat/dte-v2-1-dynamic-thinking-effort (53f22dc) into feat/dte-v…
easonliang28 Sep 4, 2026
39762bf
test(settings): assert false and unset dynamicThinkingEffort persiste…
easonliang28 Sep 4, 2026
efbd336
Merge U1 final head (39762bf81) into U2 (stack sync: U1 last CR fix —…
easonliang28 Sep 4, 2026
e89cced
Merge upstream main (0dbd5846f) into U1 (stack sync: main v3.82.0 - G…
easonliang28 Sep 5, 2026
069c34b
Merge U1 head (e89cceddd9a) into U2 (stack sync: U1 final head in - m…
easonliang28 Sep 5, 2026
9bdc3c4
Merge remote-tracking branch 'upstream/main' into feat/dte-v2-2-per-r…
Oct 2, 2026
810caf1
chore: trigger label reconcile (CI green + CR approved, cron stalled)
Oct 2, 2026
2d8feeb
Merge commit 'refs/shapes/upstream-main' into feat/dte-v2-2-per-reque…
Oct 2, 2026
77ce7d2
Merge upstream main (3859e5dd8) into feat/dte-v2-2-per-request-effort
Oct 4, 2026
e776028
Retrigger CI: mutation-diff job failed on harness errors, not mutants
Oct 4, 2026
107649b
Drop the Windows related-test discovery workaround
Oct 4, 2026
331078f
merge(upstream): take main into the per-request effort unit
Oct 6, 2026
9189386
merge(upstream): take main into DTE unit 1
Oct 6, 2026
c95c510
test(webview): assert the two different outcomes for invalid delete t…
Oct 6, 2026
48ef159
Merge upstream/main into feat/dte-v2-2-per-request-effort
Oct 7, 2026
7e4c79d
Merge upstream/main into feat/dte-v2-2-per-request-effort
Oct 7, 2026
8fc8d1e
Merge upstream/main into feat/dte-v2-1-dynamic-thinking-effort
Oct 7, 2026
01a111e
test(settings): restore the blanket auto-deny specs to their upstream…
Oct 8, 2026
5ebf096
Merge upstream main into feat/dte-v2-2-per-request-effort
easonliang28 Oct 13, 2026
0287e7e
Make the invalid-timestamp delete assertion non-vacuous
easonliang28 Oct 13, 2026
bd914cf
Merge upstream/main into feat/dte-v2-1-dynamic-thinking-effort
easonliang28 Oct 17, 2026
02d56ec
Merge feat/dte-v2-1-dynamic-thinking-effort (post-main-sync head) int…
easonliang28 Oct 17, 2026
3109dcd
style(webview): drop the duplicated blank line the merge left between…
easonliang28 Oct 17, 2026
a556b05
Merge remote-tracking branch 'upstream/main' into feat/dte-v2-2-per-r…
easonLiangWorldedtech Oct 22, 2026
7d8a620
style: apply repo Prettier to the merge resolution
easonLiangWorldedtech Oct 22, 2026
87041c0
chore(knip): mark the test-only view-identity seams @internal
easonLiangWorldedtech Oct 22, 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
19 changes: 19 additions & 0 deletions packages/types/src/__tests__/experiment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { experimentIds, experimentIdsSchema, experimentsSchema } from "../experiment.js"

describe("dynamicThinkingEffort experiment", () => {
it("is part of the experiment id enum", () => {
expect(experimentIds).toContain("dynamicThinkingEffort")
expect(experimentIdsSchema.safeParse("dynamicThinkingEffort").success).toBe(true)
})

it("parses enabled and disabled states", () => {
expect(experimentsSchema.parse({ dynamicThinkingEffort: true })).toEqual({ dynamicThinkingEffort: true })
expect(experimentsSchema.parse({ dynamicThinkingEffort: false })).toEqual({ dynamicThinkingEffort: false })
expect(experimentsSchema.parse({})).toEqual({})
})

it("rejects non-boolean values", () => {
expect(experimentsSchema.safeParse({ dynamicThinkingEffort: "yes" }).success).toBe(false)
expect(experimentIdsSchema.safeParse("dynamic-thinking-effort").success).toBe(false)
})
})
2 changes: 2 additions & 0 deletions packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const experimentIds = [
"runSlashCommand",
"customTools",
"parallelToolExecution",
"dynamicThinkingEffort",
] as const

export const experimentIdsSchema = z.enum(experimentIds)
Expand All @@ -28,6 +29,7 @@ export const experimentsSchema = z.object({
runSlashCommand: z.boolean().optional(),
customTools: z.boolean().optional(),
parallelToolExecution: z.boolean().optional(),
dynamicThinkingEffort: z.boolean().optional(),
})

export type Experiments = z.infer<typeof experimentsSchema>
Expand Down
11 changes: 9 additions & 2 deletions scripts/stryker-diff.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,15 @@ export const PACKAGE_CONFIGS = [
root: "src",
sourceRoot: "src/",
vitestConfig: "vitest.config.ts",
vitestRelated: false,
discoverRelatedTests: true,

// Use plugin-side related-test discovery: with an explicit STRYKER_TEST_FILES
// list, stryker-js 10.0.0 plans static mutants with runtime activation
// (stryker-mutator/stryker-js#6144, #6209), which the vitest plugin can only
// apply after top-level code has run, producing false Survived results.
// Revert to vitestRelated: false / discoverRelatedTests: true once the
// upstream planner fix lands.
vitestRelated: true,
discoverRelatedTests: false,
// CI build/coverage tooling under src/scripts is excluded like the other build scripts:
// it is not shipped runtime code and is covered by src/scripts/__tests__/merge-lcov.spec.mjs.
excludedPaths: [
Expand Down
8 changes: 6 additions & 2 deletions scripts/stryker-diff.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,12 @@ describe("buildManifest", () => {
assert.equal(webview.runRoot, ".")
assert.equal(webview.discoverRelatedTests, true)
assert.equal(webview.vitestRelated, false)
assert.equal(extension.discoverRelatedTests, true)
assert.equal(extension.vitestRelated, false)

// The extension entry uses plugin-side related discovery while stryker-js 10.0.0
// plans static mutants with runtime activation when given explicit test files
// (stryker-mutator/stryker-js#6144, #6209); see the entry comment in stryker-diff.mjs.
assert.equal(extension.discoverRelatedTests, false)
assert.equal(extension.vitestRelated, true)
const types = manifest.packages.find(({ id }) => id === "types")
assert.equal(types.discoverRelatedTests, undefined)
assert.equal(types.vitestRelated, undefined)
Expand Down
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,
type ModelRecord,
} from "@roo-code/types"

Expand Down Expand Up @@ -119,6 +120,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
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// npx vitest run src/api/transform/__tests__/dte-effective-reasoning-effort.spec.ts

import { ADAPTIVE_OUTPUT_CONFIG_EFFORTS, resolveEffectiveReasoningEffort } from "../reasoning"

describe("DTE series 2/5 — resolveEffectiveReasoningEffort", () => {
const settingsEffort = "high"
const modelDefault = "medium"

it("returns the per-request override when present (strongest precedence)", () => {
expect(
resolveEffectiveReasoningEffort({
override: "xhigh",
settingsReasoningEffort: settingsEffort,
modelDefaultEffort: modelDefault,
}),
).toBe("xhigh")
})

it("lets the override win even when it is out-of-range for the adaptive envelope", () => {
// "minimal" is a valid override value but outside the adaptive envelope set;
// resolution still returns it — envelope gating is the caller's concern.
expect(
resolveEffectiveReasoningEffort({
override: "minimal",
settingsReasoningEffort: settingsEffort,
modelDefaultEffort: modelDefault,
}),
).toBe("minimal")
})

it("falls back to the settings value when no override is present", () => {
expect(
resolveEffectiveReasoningEffort({ settingsReasoningEffort: "low", modelDefaultEffort: modelDefault }),
).toBe("low")
})

it("preserves the settings 'disable' sentinel when no override is present", () => {
expect(
resolveEffectiveReasoningEffort({ settingsReasoningEffort: "disable", modelDefaultEffort: modelDefault }),
).toBe("disable")
})

it("an explicit override wins over a settings 'disable' sentinel", () => {
expect(resolveEffectiveReasoningEffort({ override: "low", settingsReasoningEffort: "disable" })).toBe("low")
})

it("falls back to the model default when neither override nor settings is set", () => {
expect(resolveEffectiveReasoningEffort({ modelDefaultEffort: "low" })).toBe("low")
})

it("returns undefined when nothing is set", () => {
expect(resolveEffectiveReasoningEffort({})).toBeUndefined()
})

it("exposes exactly the in-range adaptive envelope efforts", () => {
expect([...ADAPTIVE_OUTPUT_CONFIG_EFFORTS]).toEqual(["low", "medium", "high", "xhigh", "max"])
})
})
47 changes: 47 additions & 0 deletions src/api/transform/reasoning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,53 @@ export type AnthropicProviderReasoningParams =

export type OpenAiReasoningParams = { reasoning_effort: OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"] }

/**
* DTE series 2/5 — effort levels accepted by the Claude 4.7+ adaptive-thinking
* `output_config.effort` envelope. Efforts outside this set (e.g. "none",
* "minimal", "disable") omit the envelope so the API applies its own default.
* @internal Exported for focused implementation tests.
*/
export const ADAPTIVE_OUTPUT_CONFIG_EFFORTS: readonly ReasoningEffortExtended[] = [
"low",
"medium",
"high",
"xhigh",
"max",
]

/**
* DTE series 2/5 — resolves the effective thinking effort for a single request.
*
* Resolution order (strongest first):
* 1. `override` — the per-request task-local effort
* (`ApiHandlerCreateMessageMetadata.reasoningEffort`),
* 2. `settingsReasoningEffort` — the settings-derived value,
* 3. `modelDefaultEffort` — the model's default effort.
*
* This is the single shared resolution point for the per-request override:
* providers that resolve the effective effort through it inherit the override
* without duplicating precedence logic. The override is transient (next request
* only) and never persisted to settings.
* @internal Exported for focused implementation tests.
*/
export const resolveEffectiveReasoningEffort = ({
override,
settingsReasoningEffort,
modelDefaultEffort,
}: {
override?: ReasoningEffortExtended
settingsReasoningEffort?: ReasoningEffortExtended | "disable"
modelDefaultEffort?: ReasoningEffortExtended
}): ReasoningEffortExtended | "disable" | undefined => {
if (override !== undefined) {
return override
}
if (settingsReasoningEffort !== undefined) {
return settingsReasoningEffort
}
return modelDefaultEffort
}

// Valid Gemini thinking levels for effort-based reasoning
const GEMINI_THINKING_LEVELS = ["minimal", "low", "medium", "high"] as const

Expand Down
54 changes: 50 additions & 4 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2168,6 +2168,38 @@ describe("ClineProvider", () => {
expect(state.destructiveCommandGuardEnabled).toBe(false)
})

test("getStateToPostToWebview returns the saved dynamicThinkingEffort experiment when enabled", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("experiments", {
...experimentDefault,
dynamicThinkingEffort: true,
})

const state = await provider.getStateToPostToWebview()

expect(state.experiments).toEqual({ ...experimentDefault, dynamicThinkingEffort: true })
})

test("getStateToPostToWebview returns the saved dynamicThinkingEffort experiment when explicitly disabled", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("experiments", {
...experimentDefault,
dynamicThinkingEffort: false,
})

const state = await provider.getStateToPostToWebview()

expect(state.experiments).toEqual({ ...experimentDefault, dynamicThinkingEffort: false })
})

test("getStateToPostToWebview defaults dynamicThinkingEffort to false when experiments are unset", async () => {
await provider.resolveWebviewView(mockWebviewView)

const state = await provider.getStateToPostToWebview()

expect(state.experiments).toEqual({ ...experimentDefault, dynamicThinkingEffort: false })
})

test("getStateToPostToWebview returns the saved blanket auto-deny setting", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("alwaysDenyUnapprovedCommands", true)
Expand Down Expand Up @@ -5341,21 +5373,35 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
await provider.addClineToStack(mockCline)

const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
const overwriteSpy = vi.spyOn(mockCline, "overwriteClineMessages")

// Test with negative timestamp
// A negative timestamp is still a number, so the handler proceeds and asks the webview
// to confirm the delete. No checkpoint can exist before it, so hasCheckpoint is false.
await messageHandler({
type: "deleteMessage",
value: -1000,
})

// Test with zero timestamp
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: -1000,
hasCheckpoint: false,
})

mockPostMessage.mockClear()

// 0 is falsy, so it is rejected as an invalid timestamp: an error is shown and no
// confirmation dialog is opened.
await messageHandler({
type: "deleteMessage",
value: 0,
})

// Invalid timestamps may still trigger confirmation dialog
// This is expected behavior as the system tries to process the message
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
expect.stringContaining("invalid_timestamp_for_deletion"),
)
expect(mockPostMessage).not.toHaveBeenCalled()
expect(overwriteSpy).not.toHaveBeenCalled()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
})

Expand Down
55 changes: 55 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import * as copilotProvider from "../../../api/providers/github-copilot"
import { webviewMessageHandler } from "../webviewMessageHandler"
import { registerCopilotModelBroadcast } from "../../../services/language-models/copilotModelBroadcast"
import type { ClineProvider } from "../ClineProvider"
import { experimentDefault } from "../../../shared/experiments"
import { flushModels, getModels } from "../../../api/providers/fetchers/modelCache"
import { getLMStudioModels } from "../../../api/providers/fetchers/lmstudio"
import { getCommands } from "../../../services/command/commands"
Expand Down Expand Up @@ -2971,6 +2972,60 @@ describe("webviewMessageHandler - telemetrySetting", () => {
})
})

describe("webviewMessageHandler - experiments", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("persists dynamicThinkingEffort true merged with the saved experiments", async () => {
vi.mocked(mockClineProvider.contextProxy.getValue).mockImplementation((key: string) =>
key === "experiments" ? { preventFocusDisruption: true } : undefined,
)

await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { experiments: { dynamicThinkingEffort: true } },
})

expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("experiments", {
preventFocusDisruption: true,
dynamicThinkingEffort: true,
})
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
})

it("persists dynamicThinkingEffort false over a previously enabled value", async () => {
vi.mocked(mockClineProvider.contextProxy.getValue).mockImplementation((key: string) =>
key === "experiments" ? { dynamicThinkingEffort: true } : undefined,
)

await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { experiments: { dynamicThinkingEffort: false } },
})

expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("experiments", {
dynamicThinkingEffort: false,
})
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
})

it("persists the experiment defaults when no experiments are saved yet", async () => {
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(undefined)

await webviewMessageHandler(mockClineProvider, {
type: "updateSettings",
updatedSettings: { experiments: {} },
})

expect(mockClineProvider.contextProxy.setValue).toHaveBeenCalledWith("experiments", {
...experimentDefault,
dynamicThinkingEffort: false,
})
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
})
})

describe("webviewMessageHandler - collapseCommandOutputByDefault", () => {
beforeEach(() => {
vi.clearAllMocks()
Expand Down
Loading
Loading