From a769e8aa52fe931d872928ce5afcc53eee1c5c77 Mon Sep 17 00:00:00 2001 From: Saksham Mathur Date: Mon, 2 Feb 2026 23:47:16 +0530 Subject: [PATCH 1/5] fix(editor): prevent diff view flickering with debounced open and improved editor reuse Add debouncing to rapid open calls, reuse existing diff editors to prevent visual flicker, and eliminate pre-opening files before diff commands. Changes include: 100ms debounce on open(), tracking current open path to dedupe concurrent calls, reordering close/show operations to avoid flash, and skipping unnecessary file pre-open that caused content flash before diff view appears. Also modernizes code with replaceAll and optional chaining. --- src/api/providers/openai.ts | 6 + src/integrations/editor/DiffViewProvider.ts | 113 +++++++++++++----- .../editor/__tests__/DiffViewProvider.spec.ts | 21 ++-- 3 files changed, 98 insertions(+), 42 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 8b36bce9d25..1e1ccce25ec 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -269,6 +269,12 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl text: message.reasoning, } } + if ("reasoning_content" in message && typeof message.reasoning_content === "string") { + yield { + type: "reasoning", + text: message.reasoning_content, + } + } if (message.content) { yield { type: "text", diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 0fced0a9a50..d5bfc0e6790 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -39,6 +39,13 @@ export class DiffViewProvider { private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [] private taskRef: WeakRef + // kilocode_change start: Flicker prevention + private openingPromise?: Promise + private lastOpenTime = 0 + private readonly OPEN_DEBOUNCE_MS = 100 + private currentOpenPath?: string + // kilocode_change end + constructor( private cwd: string, task: Task, @@ -47,6 +54,39 @@ export class DiffViewProvider { } async open(relPath: string): Promise { + // kilocode_change start: Flicker prevention - debounce rapid open calls + const now = Date.now() + const timeSinceLastOpen = now - this.lastOpenTime + + // If we're already opening this same path, return the existing promise + if (this.openingPromise && this.currentOpenPath === relPath) { + return this.openingPromise + } + + // If we're already editing this file, just return + if (this.isEditing && this.currentOpenPath === relPath) { + return + } + + // Debounce: if we just opened a file recently, wait a bit + if (timeSinceLastOpen < this.OPEN_DEBOUNCE_MS) { + await delay(this.OPEN_DEBOUNCE_MS - timeSinceLastOpen) + } + + // Create the opening promise + this.openingPromise = this._doOpen(relPath) + this.currentOpenPath = relPath + + try { + await this.openingPromise + } finally { + this.openingPromise = undefined + this.lastOpenTime = Date.now() + } + // kilocode_change end + } + + private async _doOpen(relPath: string): Promise { this.relPath = relPath const fileExists = this.editType === "modify" const absolutePath = path.resolve(this.cwd, relPath) @@ -56,10 +96,11 @@ export class DiffViewProvider { // contents. if (fileExists) { const existingDocument = vscode.workspace.textDocuments.find( - (doc) => doc.uri.scheme === "file" && arePathsEqual(doc.uri.fsPath, absolutePath), + (doc: { uri: { scheme: string; fsPath: string | undefined } }) => + doc.uri.scheme === "file" && arePathsEqual(doc.uri.fsPath, absolutePath), ) - if (existingDocument && existingDocument.isDirty) { + if (existingDocument?.isDirty) { await existingDocument.save() } } @@ -89,10 +130,9 @@ export class DiffViewProvider { // Close the tab if it's open (it's already saved above). const tabs = vscode.window.tabGroups.all - .map((tg) => tg.tabs) - .flat() + .flatMap((tg: { tabs: any }) => tg.tabs) .filter( - (tab) => + (tab: { input: { uri: { scheme: string; fsPath: string | undefined } } }) => tab.input instanceof vscode.TabInputText && tab.input.uri.scheme === "file" && arePathsEqual(tab.input.uri.fsPath, absolutePath), @@ -238,8 +278,11 @@ export class DiffViewProvider { await updatedDocument.save() } - await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true }) + // kilocode_change start: Prevent flicker by closing diff view before showing file + // Close diff views first, then show the file to avoid visual flicker await this.closeAllDiffViews() + await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true }) + // kilocode_change end // Getting diagnostics before and after the file edit is a better approach than // automatically tracking problems in real-time. This method ensures we only @@ -299,12 +342,19 @@ export class DiffViewProvider { const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n" // Normalize EOL characters without trimming content - const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL) + const normalizedEditedContent = editedContent.replaceAll(/\r\n|\n/g, newContentEOL) // Just in case the new content has a mix of varying EOL characters. - const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL) + const normalizedNewContent = this.newContent.replaceAll(/\r\n|\n/g, newContentEOL) - if (normalizedEditedContent !== normalizedNewContent) { + if (normalizedEditedContent === normalizedNewContent) { + // No changes to Roo's edits. + // Store the results as class properties for formatFileWriteResponse to use + this.newProblemsMessage = newProblemsMessage + this.userEdits = undefined + + return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent } + } else { // User made changes before approving edit. const userEdits = formatResponse.createPrettyPatch( this.relPath.toPosix(), @@ -317,13 +367,6 @@ export class DiffViewProvider { this.userEdits = userEdits return { newProblemsMessage, userEdits, finalContent: normalizedEditedContent } - } else { - // No changes to Roo's edits. - // Store the results as class properties for formatFileWriteResponse to use - this.newProblemsMessage = newProblemsMessage - this.userEdits = undefined - - return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent } } } @@ -519,6 +562,7 @@ export class DiffViewProvider { const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath)) + // kilocode_change start: Improved diff editor reuse to prevent flickering // If this diff editor is already open (ie if a previous write file was // interrupted) then we should activate that instead of opening a new // diff. @@ -532,9 +576,16 @@ export class DiffViewProvider { ) if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) { + // Check if we already have an active editor for this diff + const existingEditor = vscode.window.visibleTextEditors.find((e) => e.document.uri.fsPath === uri.fsPath) + if (existingEditor) { + // Reuse existing editor without switching focus + return existingEditor + } const editor = await vscode.window.showTextDocument(diffTab.input.modified, { preserveFocus: true }) return editor } + // kilocode_change end // Open new diff editor. return new Promise((resolve, reject) => { @@ -600,22 +651,19 @@ export class DiffViewProvider { }), ) - // Pre-open the file as a text document to ensure it doesn't open in preview mode - // This fixes issues with files that have custom editor associations (like markdown preview) - vscode.window - .showTextDocument(uri, { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }) - .then(() => { - // Execute the diff command after ensuring the file is open as text - return vscode.commands.executeCommand( - "vscode.diff", - vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ - query: Buffer.from(this.originalContent ?? "").toString("base64"), - }), - uri, - `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, - { preserveFocus: true }, - ) - }) + // kilocode_change start: Skip pre-opening file to prevent flicker + // Execute the diff command directly without pre-opening the file + // This prevents the flash of the file content before the diff view appears + vscode.commands + .executeCommand( + "vscode.diff", + vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ + query: Buffer.from(this.originalContent ?? "").toString("base64"), + }), + uri, + `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, + { preserveFocus: true }, + ) .then( () => { // Command executed successfully, now wait for the editor to appear @@ -625,6 +673,7 @@ export class DiffViewProvider { reject(new Error(`Failed to execute diff command for ${uri.fsPath}: ${err.message}`)) }, ) + // kilocode_change end }) } diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index bc3391609c9..a4657d44c13 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -234,11 +234,11 @@ describe("DiffViewProvider", () => { // Execute open await diffViewProvider.open("test.md") - // Verify that showTextDocument was called before executeCommand - expect(callOrder).toEqual(["showTextDocument", "executeCommand"]) + // kilocode_change: Updated to reflect flicker-free behavior (no pre-opening) + expect(callOrder).toEqual(["executeCommand"]) - // Verify that showTextDocument was called with preview: false and preserveFocus: true - expect(vscode.window.showTextDocument).toHaveBeenCalledWith( + // Verify that showTextDocument was NOT called during open (prevents flicker) + expect(vscode.window.showTextDocument).not.toHaveBeenCalledWith( expect.objectContaining({ fsPath: `${mockCwd}/test.md` }), { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }, ) @@ -253,22 +253,23 @@ describe("DiffViewProvider", () => { ) }) - it("should handle showTextDocument failure", async () => { - // Mock showTextDocument to fail - vi.mocked(vscode.window.showTextDocument).mockRejectedValue(new Error("Cannot open file")) - + // kilocode_change: Updated test for new flicker-free behavior + it("should handle diff editor timeout", async () => { // Mock workspace.onDidOpenTextDocument vi.mocked(vscode.workspace.onDidOpenTextDocument).mockReturnValue({ dispose: vi.fn() }) // Mock window.onDidChangeVisibleTextEditors vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) + // Mock executeCommand to succeed but editor never appears + vi.mocked(vscode.commands.executeCommand).mockResolvedValue(undefined) + // Set up for file ;(diffViewProvider as any).editType = "modify" - // Try to open and expect rejection + // Try to open and expect timeout error await expect(diffViewProvider.open("test.md")).rejects.toThrow( - "Failed to execute diff command for /mock/cwd/test.md: Cannot open file", + "Failed to open diff editor for /mock/cwd/test.md within 10 seconds", ) }) }) From 6909b35b3c3b44a4cd3fe3d7fb203e7f81b52c14 Mon Sep 17 00:00:00 2001 From: Saksham Mathur Date: Mon, 2 Feb 2026 23:59:01 +0530 Subject: [PATCH 2/5] revert --- src/integrations/editor/DiffViewProvider.ts | 117 +++++------------- .../editor/__tests__/DiffViewProvider.spec.ts | 21 ++-- 2 files changed, 44 insertions(+), 94 deletions(-) diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index d5bfc0e6790..7ab034455ca 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -6,13 +6,13 @@ import stripBom from "strip-bom" import { XMLBuilder } from "fast-xml-parser" import delay from "delay" -import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS, isNativeProtocol } from "@roo-code/types" - import { createDirectoriesForFile } from "../../utils/fs" import { arePathsEqual, getReadablePath } from "../../utils/path" import { formatResponse } from "../../core/prompts/responses" import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics" +import { ClineSayTool } from "../../shared/ExtensionMessage" import { Task } from "../../core/task/Task" +import { DEFAULT_WRITE_DELAY_MS, isNativeProtocol } from "@roo-code/types" import { resolveToolProtocol } from "../../utils/resolveToolProtocol" import { DecorationController } from "./DecorationController" @@ -39,13 +39,6 @@ export class DiffViewProvider { private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [] private taskRef: WeakRef - // kilocode_change start: Flicker prevention - private openingPromise?: Promise - private lastOpenTime = 0 - private readonly OPEN_DEBOUNCE_MS = 100 - private currentOpenPath?: string - // kilocode_change end - constructor( private cwd: string, task: Task, @@ -54,39 +47,6 @@ export class DiffViewProvider { } async open(relPath: string): Promise { - // kilocode_change start: Flicker prevention - debounce rapid open calls - const now = Date.now() - const timeSinceLastOpen = now - this.lastOpenTime - - // If we're already opening this same path, return the existing promise - if (this.openingPromise && this.currentOpenPath === relPath) { - return this.openingPromise - } - - // If we're already editing this file, just return - if (this.isEditing && this.currentOpenPath === relPath) { - return - } - - // Debounce: if we just opened a file recently, wait a bit - if (timeSinceLastOpen < this.OPEN_DEBOUNCE_MS) { - await delay(this.OPEN_DEBOUNCE_MS - timeSinceLastOpen) - } - - // Create the opening promise - this.openingPromise = this._doOpen(relPath) - this.currentOpenPath = relPath - - try { - await this.openingPromise - } finally { - this.openingPromise = undefined - this.lastOpenTime = Date.now() - } - // kilocode_change end - } - - private async _doOpen(relPath: string): Promise { this.relPath = relPath const fileExists = this.editType === "modify" const absolutePath = path.resolve(this.cwd, relPath) @@ -96,11 +56,10 @@ export class DiffViewProvider { // contents. if (fileExists) { const existingDocument = vscode.workspace.textDocuments.find( - (doc: { uri: { scheme: string; fsPath: string | undefined } }) => - doc.uri.scheme === "file" && arePathsEqual(doc.uri.fsPath, absolutePath), + (doc) => doc.uri.scheme === "file" && arePathsEqual(doc.uri.fsPath, absolutePath), ) - if (existingDocument?.isDirty) { + if (existingDocument && existingDocument.isDirty) { await existingDocument.save() } } @@ -130,9 +89,10 @@ export class DiffViewProvider { // Close the tab if it's open (it's already saved above). const tabs = vscode.window.tabGroups.all - .flatMap((tg: { tabs: any }) => tg.tabs) + .map((tg) => tg.tabs) + .flat() .filter( - (tab: { input: { uri: { scheme: string; fsPath: string | undefined } } }) => + (tab) => tab.input instanceof vscode.TabInputText && tab.input.uri.scheme === "file" && arePathsEqual(tab.input.uri.fsPath, absolutePath), @@ -278,11 +238,8 @@ export class DiffViewProvider { await updatedDocument.save() } - // kilocode_change start: Prevent flicker by closing diff view before showing file - // Close diff views first, then show the file to avoid visual flicker - await this.closeAllDiffViews() await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true }) - // kilocode_change end + await this.closeAllDiffViews() // Getting diagnostics before and after the file edit is a better approach than // automatically tracking problems in real-time. This method ensures we only @@ -342,19 +299,12 @@ export class DiffViewProvider { const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n" // Normalize EOL characters without trimming content - const normalizedEditedContent = editedContent.replaceAll(/\r\n|\n/g, newContentEOL) + const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL) // Just in case the new content has a mix of varying EOL characters. - const normalizedNewContent = this.newContent.replaceAll(/\r\n|\n/g, newContentEOL) + const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL) - if (normalizedEditedContent === normalizedNewContent) { - // No changes to Roo's edits. - // Store the results as class properties for formatFileWriteResponse to use - this.newProblemsMessage = newProblemsMessage - this.userEdits = undefined - - return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent } - } else { + if (normalizedEditedContent !== normalizedNewContent) { // User made changes before approving edit. const userEdits = formatResponse.createPrettyPatch( this.relPath.toPosix(), @@ -367,6 +317,13 @@ export class DiffViewProvider { this.userEdits = userEdits return { newProblemsMessage, userEdits, finalContent: normalizedEditedContent } + } else { + // No changes to Roo's edits. + // Store the results as class properties for formatFileWriteResponse to use + this.newProblemsMessage = newProblemsMessage + this.userEdits = undefined + + return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent } } } @@ -562,7 +519,6 @@ export class DiffViewProvider { const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath)) - // kilocode_change start: Improved diff editor reuse to prevent flickering // If this diff editor is already open (ie if a previous write file was // interrupted) then we should activate that instead of opening a new // diff. @@ -576,16 +532,9 @@ export class DiffViewProvider { ) if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) { - // Check if we already have an active editor for this diff - const existingEditor = vscode.window.visibleTextEditors.find((e) => e.document.uri.fsPath === uri.fsPath) - if (existingEditor) { - // Reuse existing editor without switching focus - return existingEditor - } const editor = await vscode.window.showTextDocument(diffTab.input.modified, { preserveFocus: true }) return editor } - // kilocode_change end // Open new diff editor. return new Promise((resolve, reject) => { @@ -651,19 +600,22 @@ export class DiffViewProvider { }), ) - // kilocode_change start: Skip pre-opening file to prevent flicker - // Execute the diff command directly without pre-opening the file - // This prevents the flash of the file content before the diff view appears - vscode.commands - .executeCommand( - "vscode.diff", - vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ - query: Buffer.from(this.originalContent ?? "").toString("base64"), - }), - uri, - `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, - { preserveFocus: true }, - ) + // Pre-open the file as a text document to ensure it doesn't open in preview mode + // This fixes issues with files that have custom editor associations (like markdown preview) + vscode.window + .showTextDocument(uri, { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }) + .then(() => { + // Execute the diff command after ensuring the file is open as text + return vscode.commands.executeCommand( + "vscode.diff", + vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ + query: Buffer.from(this.originalContent ?? "").toString("base64"), + }), + uri, + `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, + { preserveFocus: true }, + ) + }) .then( () => { // Command executed successfully, now wait for the editor to appear @@ -673,7 +625,6 @@ export class DiffViewProvider { reject(new Error(`Failed to execute diff command for ${uri.fsPath}: ${err.message}`)) }, ) - // kilocode_change end }) } diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index a4657d44c13..bc3391609c9 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -234,11 +234,11 @@ describe("DiffViewProvider", () => { // Execute open await diffViewProvider.open("test.md") - // kilocode_change: Updated to reflect flicker-free behavior (no pre-opening) - expect(callOrder).toEqual(["executeCommand"]) + // Verify that showTextDocument was called before executeCommand + expect(callOrder).toEqual(["showTextDocument", "executeCommand"]) - // Verify that showTextDocument was NOT called during open (prevents flicker) - expect(vscode.window.showTextDocument).not.toHaveBeenCalledWith( + // Verify that showTextDocument was called with preview: false and preserveFocus: true + expect(vscode.window.showTextDocument).toHaveBeenCalledWith( expect.objectContaining({ fsPath: `${mockCwd}/test.md` }), { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }, ) @@ -253,23 +253,22 @@ describe("DiffViewProvider", () => { ) }) - // kilocode_change: Updated test for new flicker-free behavior - it("should handle diff editor timeout", async () => { + it("should handle showTextDocument failure", async () => { + // Mock showTextDocument to fail + vi.mocked(vscode.window.showTextDocument).mockRejectedValue(new Error("Cannot open file")) + // Mock workspace.onDidOpenTextDocument vi.mocked(vscode.workspace.onDidOpenTextDocument).mockReturnValue({ dispose: vi.fn() }) // Mock window.onDidChangeVisibleTextEditors vi.mocked(vscode.window.onDidChangeVisibleTextEditors).mockReturnValue({ dispose: vi.fn() }) - // Mock executeCommand to succeed but editor never appears - vi.mocked(vscode.commands.executeCommand).mockResolvedValue(undefined) - // Set up for file ;(diffViewProvider as any).editType = "modify" - // Try to open and expect timeout error + // Try to open and expect rejection await expect(diffViewProvider.open("test.md")).rejects.toThrow( - "Failed to open diff editor for /mock/cwd/test.md within 10 seconds", + "Failed to execute diff command for /mock/cwd/test.md: Cannot open file", ) }) }) From f4221341ef603ab5956dd1a190aba43c6df9c0b7 Mon Sep 17 00:00:00 2001 From: Saksham Mathur Date: Tue, 3 Feb 2026 00:15:52 +0530 Subject: [PATCH 3/5] feat(api): add Kimi reasoning parameter support Add support for Kimi-specific thinking parameter format by setting `thinking: { type: "enabled" }` when reasoning budget is enabled. Also preserve `reasoning_content` for Kimi/DeepSeek interleaved thinking in the message transformation. --- src/api/providers/moonshot.ts | 8 +++++++- src/api/transform/openai-format.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/api/providers/moonshot.ts b/src/api/providers/moonshot.ts index d29a10a3b3e..de9c5b858e0 100644 --- a/src/api/providers/moonshot.ts +++ b/src/api/providers/moonshot.ts @@ -38,7 +38,7 @@ export class MoonshotHandler extends OpenAiHandler { } } - // Override to always include max_tokens for Moonshot (not max_completion_tokens) + // Override to add Kimi-specific thinking parameter format protected override addMaxTokensIfNeeded( requestOptions: | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming @@ -47,5 +47,11 @@ export class MoonshotHandler extends OpenAiHandler { ): void { // Moonshot uses max_tokens instead of max_completion_tokens requestOptions.max_tokens = this.options.modelMaxTokens || modelInfo.maxTokens + + // For Kimi models with reasoning budget, use { type: "enabled" } instead of { max_tokens: ... } + const { info: model } = this.getModel() + if (this.options.enableReasoningEffort && (model as any).supportsReasoningBudget) { + ;(requestOptions as any).thinking = { type: "enabled" } + } } } diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index 5d96a3de36f..1636605eac8 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -317,6 +317,10 @@ export function convertToOpenAiMessages( if (mapped) { ;(baseMessage as any).reasoning_details = mapped } + // Preserve reasoning_content for Kimi/DeepSeek interleaved thinking + if (messageWithDetails.reasoning_content) { + ;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content + } } openAiMessages.push(baseMessage) @@ -502,6 +506,11 @@ export function convertToOpenAiMessages( baseMessage.reasoning_details = mapped } + // Preserve reasoning_content for Kimi/DeepSeek interleaved thinking + if (messageWithDetails.reasoning_content) { + ;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content + } + // Add tool_calls after reasoning_details // Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty if (tool_calls.length > 0) { From f697f6aa4b24b58502e25f3cff09d715fe6bde78 Mon Sep 17 00:00:00 2001 From: Saksham Mathur Date: Tue, 3 Feb 2026 01:20:48 +0530 Subject: [PATCH 4/5] fix(api): handle reasoning content correctly for moonshot with tool calls Fixes reasoning parameter format and content preservation for Kimi (Moonshot) models when tool calls are present. Changes include: - Use Kimi's native `thinking` parameter instead of OpenAI-style `reasoning` - Track and associate reasoning content with tool calls in stream processing - Preserve `reasoning_content` in conversation history for Moonshot provider --- src/api/providers/moonshot.ts | 2 ++ src/api/providers/openai.ts | 8 ++++- src/core/task/Task.ts | 55 +++++++++++++++++++++++++++-------- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/api/providers/moonshot.ts b/src/api/providers/moonshot.ts index de9c5b858e0..174fb99b10a 100644 --- a/src/api/providers/moonshot.ts +++ b/src/api/providers/moonshot.ts @@ -51,6 +51,8 @@ export class MoonshotHandler extends OpenAiHandler { // For Kimi models with reasoning budget, use { type: "enabled" } instead of { max_tokens: ... } const { info: model } = this.getModel() if (this.options.enableReasoningEffort && (model as any).supportsReasoningBudget) { + // Remove the OpenAI-style reasoning parameter and use Kimi's thinking parameter + delete (requestOptions as any).reasoning ;(requestOptions as any).thinking = { type: "enabled" } } } diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 1e1ccce25ec..41d95acbcac 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -193,6 +193,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl let lastUsage const activeToolCallIds = new Set() + // Track reasoning content for Kimi/DeepSeek to associate with tool calls + let pendingReasoningContent: string | undefined for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta ?? {} @@ -212,6 +214,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ? delta.reasoning : undefined if (reasoningText) { + pendingReasoningContent = reasoningText yield { type: "reasoning", text: reasoningText, @@ -219,7 +222,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } // kilocode_change end - yield* this.processToolCalls(delta, finishReason, activeToolCallIds) + yield* this.processToolCalls(delta, finishReason, activeToolCallIds, pendingReasoningContent) + pendingReasoningContent = undefined if (chunk.usage) { lastUsage = chunk.usage @@ -503,11 +507,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl * @param delta - The delta object from the stream chunk * @param finishReason - The finish_reason from the stream chunk * @param activeToolCallIds - Set to track active tool call IDs (mutated in place) + * @param reasoningContent - Optional reasoning content to include with tool calls (for Kimi/DeepSeek) */ private *processToolCalls( delta: OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta | undefined, finishReason: string | null | undefined, activeToolCallIds: Set, + reasoningContent?: string, ): Generator< | { type: "tool_call_partial"; index: number; id?: string; name?: string; arguments?: string } | { type: "tool_call_end"; id: string } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 21f0d6c5434..6e9b6f9b3e3 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1088,6 +1088,17 @@ export class Task extends EventEmitter implements TaskLike { } else if (!messageWithTs.content) { messageWithTs.content = [reasoningBlock] } + + // For Kimi (Moonshot), add reasoning_content as top-level property when there are tool_use blocks + // This is required because Kimi's API expects: "reasoning_content" + "tool_calls" in the same message + if ( + this.apiConfiguration.apiProvider === "moonshot" && + reasoning && + Array.isArray(messageWithTs.content) && + messageWithTs.content.some((block: any) => block.type === "tool_use") + ) { + messageWithTs.reasoning_content = reasoning + } } else if (reasoningData?.encrypted_content) { // OpenAI Native encrypted reasoning const reasoningBlock = { @@ -4847,11 +4858,16 @@ export class Task extends EventEmitter implements TaskLike { } // Create message with reasoning_details property - cleanConversationHistory.push({ + const msgWithReasoningContent: Anthropic.Messages.MessageParam & { reasoning_content?: string } = { role: "assistant", content: assistantContent, reasoning_details: msgWithDetails.reasoning_details, - } as any) + } + // Preserve reasoning_content for Kimi (Moonshot) + if (msg.reasoning_content) { + msgWithReasoningContent.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(msgWithReasoningContent) continue } @@ -4884,10 +4900,15 @@ export class Task extends EventEmitter implements TaskLike { assistantContent = rest } - cleanConversationHistory.push({ - role: "assistant", - content: assistantContent, - } satisfies Anthropic.Messages.MessageParam) + const msgWithEncryptedReasoning: Anthropic.Messages.MessageParam & { reasoning_content?: string } = + { + role: "assistant", + content: assistantContent, + } + if (msg.reasoning_content) { + msgWithEncryptedReasoning.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(msgWithEncryptedReasoning) continue } else if (hasPlainTextReasoning) { @@ -4911,10 +4932,15 @@ export class Task extends EventEmitter implements TaskLike { } } - cleanConversationHistory.push({ - role: "assistant", - content: assistantContent, - } satisfies Anthropic.Messages.MessageParam) + const msgWithPlainTextReasoning: Anthropic.Messages.MessageParam & { reasoning_content?: string } = + { + role: "assistant", + content: assistantContent, + } + if (msg.reasoning_content) { + msgWithPlainTextReasoning.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(msgWithPlainTextReasoning) continue } @@ -4922,10 +4948,15 @@ export class Task extends EventEmitter implements TaskLike { // Default path for regular messages (no embedded reasoning) if (msg.role) { - cleanConversationHistory.push({ + const baseMessage: Anthropic.Messages.MessageParam & { reasoning_content?: string } = { role: msg.role, content: msg.content as Anthropic.Messages.ContentBlockParam[] | string, - }) + } + // Preserve reasoning_content for Kimi (Moonshot) when present + if (msg.reasoning_content) { + baseMessage.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(baseMessage) } } From 3c095b35eea49a65310b364e338f0ff1ce960133 Mon Sep 17 00:00:00 2001 From: Saksham Mathur Date: Tue, 3 Feb 2026 01:26:11 +0530 Subject: [PATCH 5/5] fix(types): add reasoning_details to type annotation for Moonshot messages --- src/core/task/Task.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 6e9b6f9b3e3..aa2d70ebc91 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -4858,7 +4858,10 @@ export class Task extends EventEmitter implements TaskLike { } // Create message with reasoning_details property - const msgWithReasoningContent: Anthropic.Messages.MessageParam & { reasoning_content?: string } = { + const msgWithReasoningContent: Anthropic.Messages.MessageParam & { + reasoning_content?: string + reasoning_details?: any + } = { role: "assistant", content: assistantContent, reasoning_details: msgWithDetails.reasoning_details,