From 7534e19c606726b0d8c6a06c04cd813f9e1889cd Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 26 Jan 2026 15:26:47 -0700 Subject: [PATCH 001/256] chore: remove POWER_STEERING experiment remnants (#10980) --- packages/types/src/experiment.ts | 2 -- src/shared/__tests__/experiments.spec.ts | 21 ++++++++----------- src/shared/experiments.ts | 2 -- .../__tests__/ExtensionStateContext.spec.tsx | 8 ------- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index 201153917c..cbdba55f0a 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -7,7 +7,6 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" */ export const experimentIds = [ - "powerSteering", "preventFocusDisruption", "imageGeneration", "runSlashCommand", @@ -24,7 +23,6 @@ export type ExperimentId = z.infer */ export const experimentsSchema = z.object({ - powerSteering: z.boolean().optional(), preventFocusDisruption: z.boolean().optional(), imageGeneration: z.boolean().optional(), runSlashCommand: z.boolean().optional(), diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 9f5a7469a8..25f2667830 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -5,50 +5,47 @@ import type { ExperimentId } from "@roo-code/types" import { EXPERIMENT_IDS, experimentConfigsMap, experiments as Experiments } from "../experiments" describe("experiments", () => { - describe("POWER_STEERING", () => { + describe("PREVENT_FOCUS_DISRUPTION", () => { it("is configured correctly", () => { - expect(EXPERIMENT_IDS.POWER_STEERING).toBe("powerSteering") - expect(experimentConfigsMap.POWER_STEERING).toMatchObject({ + expect(EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION).toBe("preventFocusDisruption") + expect(experimentConfigsMap.PREVENT_FOCUS_DISRUPTION).toMatchObject({ enabled: false, }) }) }) describe("isEnabled", () => { - it("returns false when POWER_STEERING experiment is not enabled", () => { + it("returns false when experiment is not enabled", () => { const experiments: Record = { - powerSteering: false, preventFocusDisruption: false, imageGeneration: false, runSlashCommand: false, multipleNativeToolCalls: false, customTools: false, } - expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) }) - it("returns true when experiment POWER_STEERING is enabled", () => { + it("returns true when experiment is enabled", () => { const experiments: Record = { - powerSteering: true, - preventFocusDisruption: false, + preventFocusDisruption: true, imageGeneration: false, runSlashCommand: false, multipleNativeToolCalls: false, customTools: false, } - expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(true) }) it("returns false when experiment is not present", () => { const experiments: Record = { - powerSteering: false, preventFocusDisruption: false, imageGeneration: false, runSlashCommand: false, multipleNativeToolCalls: false, customTools: false, } - expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) }) }) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 1215040d0e..85a767cb14 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -1,7 +1,6 @@ import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } from "@roo-code/types" export const EXPERIMENT_IDS = { - POWER_STEERING: "powerSteering", PREVENT_FOCUS_DISRUPTION: "preventFocusDisruption", IMAGE_GENERATION: "imageGeneration", RUN_SLASH_COMMAND: "runSlashCommand", @@ -18,7 +17,6 @@ interface ExperimentConfig { } export const experimentConfigsMap: Record = { - POWER_STEERING: { enabled: false }, PREVENT_FOCUS_DISRUPTION: { enabled: false }, IMAGE_GENERATION: { enabled: false }, RUN_SLASH_COMMAND: { enabled: false }, diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 6589077cfb..0ee69a4ad6 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -233,10 +233,6 @@ describe("mergeExtensionState", () => { ...baseState, apiConfiguration: { modelMaxThinkingTokens: 456, modelTemperature: 0.3 }, experiments: { - powerSteering: true, - marketplace: false, - disableCompletionCommand: false, - concurrentFileReads: true, preventFocusDisruption: false, imageGeneration: false, runSlashCommand: false, @@ -254,10 +250,6 @@ describe("mergeExtensionState", () => { }) expect(result.experiments).toEqual({ - powerSteering: true, - marketplace: false, - disableCompletionCommand: false, - concurrentFileReads: true, preventFocusDisruption: false, imageGeneration: false, runSlashCommand: false, From bd297664069cca29213b5c4aa764025e6ab6084e Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 26 Jan 2026 17:19:27 -0700 Subject: [PATCH 002/256] fix: record truncation event when condensation fails but truncation succeeds (#10984) --- src/core/task/Task.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d2be23714e..8482369bd4 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -4012,7 +4012,8 @@ export class Task extends EventEmitter implements TaskLike { } if (truncateResult.error) { await this.say("condense_context_error", truncateResult.error) - } else if (truncateResult.summary) { + } + if (truncateResult.summary) { const { summary, cost, prevContextTokens, newContextTokens = 0, condenseId } = truncateResult const contextCondense: ContextCondense = { summary, From 27708f303876118e13e0587a0311b26702892f07 Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:21:49 -0500 Subject: [PATCH 003/256] feat: new_task tool creates checkpoint the same way write_to_file does (#10982) --- src/core/assistant-message/presentAssistantMessage.ts | 1 + src/core/tools/NewTaskTool.ts | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index e3f652d352..231901e0df 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -862,6 +862,7 @@ export async function presentAssistantMessage(cline: Task) { }) break case "new_task": + await checkpointSaveAndMark(cline) await newTaskTool.handle(cline, block as ToolUse<"new_task">, { askApproval, handleError, diff --git a/src/core/tools/NewTaskTool.ts b/src/core/tools/NewTaskTool.ts index fd208128da..f36d8e1e37 100644 --- a/src/core/tools/NewTaskTool.ts +++ b/src/core/tools/NewTaskTool.ts @@ -109,12 +109,6 @@ export class NewTaskTool extends BaseTool<"new_task"> { return } - // Provider is guaranteed to be defined here due to earlier check. - - if (task.enableCheckpoints) { - task.checkpointSave(true) - } - // Delegate parent and open child as sole active task const child = await (provider as any).delegateParentAndOpenChild({ parentTaskId: task.taskId, From dd245cc40cd9fd49b2955ed9bcad28ad7df62755 Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:43:10 -0500 Subject: [PATCH 004/256] fix: VS Code LM token counting returns 0 outside requests, breaking context condensing (EXT-620) (#10983) - Modified VsCodeLmHandler.internalCountTokens() to create temporary cancellation tokens when needed - Token counting now works both during and outside of active requests - Added 4 new tests to verify the fix and prevent regression - Resolves issue where VS Code LM API users experienced context overflow errors --- src/api/providers/__tests__/vscode-lm.spec.ts | 60 +++++++++++++++++++ src/api/providers/vscode-lm.ts | 25 +++++--- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/api/providers/__tests__/vscode-lm.spec.ts b/src/api/providers/__tests__/vscode-lm.spec.ts index 9c050b5bc6..305305d228 100644 --- a/src/api/providers/__tests__/vscode-lm.spec.ts +++ b/src/api/providers/__tests__/vscode-lm.spec.ts @@ -437,6 +437,66 @@ describe("VsCodeLmHandler", () => { }) }) + describe("countTokens", () => { + beforeEach(() => { + handler["client"] = mockLanguageModelChat + }) + + it("should count tokens when called outside of an active request", async () => { + // Ensure no active request cancellation token exists + handler["currentRequestCancellation"] = null + + mockLanguageModelChat.countTokens.mockResolvedValueOnce(42) + + const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello world" }] + const result = await handler.countTokens(content) + + expect(result).toBe(42) + expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Hello world", expect.any(Object)) + }) + + it("should count tokens when called during an active request", async () => { + // Simulate an active request with a cancellation token + const mockCancellation = { + token: { isCancellationRequested: false, onCancellationRequested: vi.fn() }, + cancel: vi.fn(), + dispose: vi.fn(), + } + handler["currentRequestCancellation"] = mockCancellation as any + + mockLanguageModelChat.countTokens.mockResolvedValueOnce(50) + + const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Test content" }] + const result = await handler.countTokens(content) + + expect(result).toBe(50) + expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Test content", mockCancellation.token) + }) + + it("should return 0 when no client is available", async () => { + handler["client"] = null + handler["currentRequestCancellation"] = null + + const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello" }] + const result = await handler.countTokens(content) + + expect(result).toBe(0) + }) + + it("should handle image blocks with placeholder", async () => { + handler["currentRequestCancellation"] = null + mockLanguageModelChat.countTokens.mockResolvedValueOnce(5) + + const content: Anthropic.Messages.ContentBlockParam[] = [ + { type: "image", source: { type: "base64", media_type: "image/png", data: "abc" } }, + ] + const result = await handler.countTokens(content) + + expect(result).toBe(5) + expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("[IMAGE]", expect.any(Object)) + }) + }) + describe("completePrompt", () => { it("should complete single prompt", async () => { const mockModel = { ...mockLanguageModelChat } diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index a77d326e59..8fb564a9d5 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -229,23 +229,29 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan return 0 } - if (!this.currentRequestCancellation) { - console.warn("Roo Code : No cancellation token available for token counting") - return 0 - } - // Validate input if (!text) { console.debug("Roo Code : Empty text provided for token counting") return 0 } + // Create a temporary cancellation token if we don't have one (e.g., when called outside a request) + let cancellationToken: vscode.CancellationToken + let tempCancellation: vscode.CancellationTokenSource | null = null + + if (this.currentRequestCancellation) { + cancellationToken = this.currentRequestCancellation.token + } else { + tempCancellation = new vscode.CancellationTokenSource() + cancellationToken = tempCancellation.token + } + try { // Handle different input types let tokenCount: number if (typeof text === "string") { - tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token) + tokenCount = await this.client.countTokens(text, cancellationToken) } else if (text instanceof vscode.LanguageModelChatMessage) { // For chat messages, ensure we have content if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) { @@ -253,7 +259,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan return 0 } const countMessage = extractTextCountFromMessage(text) - tokenCount = await this.client.countTokens(countMessage, this.currentRequestCancellation.token) + tokenCount = await this.client.countTokens(countMessage, cancellationToken) } else { console.warn("Roo Code : Invalid input type for token counting") return 0 @@ -287,6 +293,11 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } return 0 // Fallback to prevent stream interruption + } finally { + // Clean up temporary cancellation token + if (tempCancellation) { + tempCancellation.dispose() + } } } From 2f92cb7a8d645f0e50865f5c4043f00c2ada03eb Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 26 Jan 2026 20:34:04 -0700 Subject: [PATCH 005/256] fix: prevent nested condensing from including previously-condensed content (#10985) --- src/core/condense/__tests__/condense.spec.ts | 17 +- src/core/condense/__tests__/index.spec.ts | 28 ++- .../__tests__/nested-condense.spec.ts | 211 ++++++++++++++++++ .../__tests__/rewind-after-condense.spec.ts | 47 ++-- src/core/condense/index.ts | 34 +-- .../__tests__/context-management.spec.ts | 16 +- .../webviewMessageHandler.delete.spec.ts | 38 ++-- 7 files changed, 284 insertions(+), 107 deletions(-) create mode 100644 src/core/condense/__tests__/nested-condense.spec.ts diff --git a/src/core/condense/__tests__/condense.spec.ts b/src/core/condense/__tests__/condense.spec.ts index dcb05cd74d..9d3352d01a 100644 --- a/src/core/condense/__tests__/condense.spec.ts +++ b/src/core/condense/__tests__/condense.spec.ts @@ -250,7 +250,7 @@ Line 2 it("should not summarize messages that already contain a recent summary with no new messages", async () => { const messages: ApiMessage[] = [ { role: "user", content: "First message with /command" }, - { role: "assistant", content: "Previous summary", isSummary: true }, + { role: "user", content: "Previous summary", isSummary: true }, ] const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) @@ -413,20 +413,5 @@ Line 2 expect(result[1]).toEqual(messages[4]) expect(result[2]).toEqual(messages[5]) }) - - it("should prepend first user message when summary starts with assistant", () => { - const messages: ApiMessage[] = [ - { role: "user", content: "Original first message" }, - { role: "assistant", content: "Summary content", isSummary: true }, - { role: "user", content: "After summary" }, - ] - - const result = getMessagesSinceLastSummary(messages) - - // Should prepend original first message for Bedrock compatibility - expect(result[0]).toEqual(messages[0]) // Original first user message - expect(result[1]).toEqual(messages[1]) // The summary - expect(result[2]).toEqual(messages[2]) - }) }) }) diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts index 8a0f3bea63..c8bc5ee8ef 100644 --- a/src/core/condense/__tests__/index.spec.ts +++ b/src/core/condense/__tests__/index.spec.ts @@ -252,38 +252,36 @@ describe("getMessagesSinceLastSummary", () => { expect(result).toEqual(messages) }) - it("should return messages since the last summary (preserves original first user message when needed)", () => { + it("should return messages since the last summary", () => { const messages: ApiMessage[] = [ { role: "user", content: "Hello", ts: 1 }, { role: "assistant", content: "Hi there", ts: 2 }, - { role: "assistant", content: "Summary of conversation", ts: 3, isSummary: true }, - { role: "user", content: "How are you?", ts: 4 }, - { role: "assistant", content: "I'm good", ts: 5 }, + { role: "user", content: "Summary of conversation", ts: 3, isSummary: true }, + { role: "assistant", content: "How are you?", ts: 4 }, + { role: "user", content: "I'm good", ts: 5 }, ] const result = getMessagesSinceLastSummary(messages) expect(result).toEqual([ - { role: "user", content: "Hello", ts: 1 }, - { role: "assistant", content: "Summary of conversation", ts: 3, isSummary: true }, - { role: "user", content: "How are you?", ts: 4 }, - { role: "assistant", content: "I'm good", ts: 5 }, + { role: "user", content: "Summary of conversation", ts: 3, isSummary: true }, + { role: "assistant", content: "How are you?", ts: 4 }, + { role: "user", content: "I'm good", ts: 5 }, ]) }) it("should handle multiple summary messages and return since the last one", () => { const messages: ApiMessage[] = [ { role: "user", content: "Hello", ts: 1 }, - { role: "assistant", content: "First summary", ts: 2, isSummary: true }, - { role: "user", content: "How are you?", ts: 3 }, - { role: "assistant", content: "Second summary", ts: 4, isSummary: true }, - { role: "user", content: "What's new?", ts: 5 }, + { role: "user", content: "First summary", ts: 2, isSummary: true }, + { role: "assistant", content: "How are you?", ts: 3 }, + { role: "user", content: "Second summary", ts: 4, isSummary: true }, + { role: "assistant", content: "What's new?", ts: 5 }, ] const result = getMessagesSinceLastSummary(messages) expect(result).toEqual([ - { role: "user", content: "Hello", ts: 1 }, - { role: "assistant", content: "Second summary", ts: 4, isSummary: true }, - { role: "user", content: "What's new?", ts: 5 }, + { role: "user", content: "Second summary", ts: 4, isSummary: true }, + { role: "assistant", content: "What's new?", ts: 5 }, ]) }) diff --git a/src/core/condense/__tests__/nested-condense.spec.ts b/src/core/condense/__tests__/nested-condense.spec.ts new file mode 100644 index 0000000000..3868a22262 --- /dev/null +++ b/src/core/condense/__tests__/nested-condense.spec.ts @@ -0,0 +1,211 @@ +import { describe, it, expect } from "vitest" +import { ApiMessage } from "../../task-persistence/apiMessages" +import { getEffectiveApiHistory, getMessagesSinceLastSummary } from "../index" + +describe("nested condensing scenarios", () => { + describe("fresh-start model (user-role summaries)", () => { + it("should return only the latest summary and messages after it", () => { + const condenseId1 = "condense-1" + const condenseId2 = "condense-2" + + // Simulate history after two nested condenses with user-role summaries + const history: ApiMessage[] = [ + // Original task - condensed in first condense + { role: "user", content: "Build an app", ts: 100, condenseParent: condenseId1 }, + // Messages from first condense + { role: "assistant", content: "Starting...", ts: 200, condenseParent: condenseId1 }, + { role: "user", content: "Add auth", ts: 300, condenseParent: condenseId1 }, + // First summary (user role, fresh-start model) - then condensed in second condense + { + role: "user", + content: [{ type: "text", text: "## Summary 1" }], + ts: 399, + isSummary: true, + condenseId: condenseId1, + condenseParent: condenseId2, // Tagged during second condense + }, + // Messages after first condense but before second + { role: "assistant", content: "Auth added", ts: 400, condenseParent: condenseId2 }, + { role: "user", content: "Add database", ts: 500, condenseParent: condenseId2 }, + // Second summary (user role, fresh-start model) + { + role: "user", + content: [{ type: "text", text: "## Summary 2" }], + ts: 599, + isSummary: true, + condenseId: condenseId2, + }, + // Messages after second condense (kept messages) + { role: "assistant", content: "Database added", ts: 600 }, + { role: "user", content: "Now test it", ts: 700 }, + ] + + // Step 1: Get effective history + const effectiveHistory = getEffectiveApiHistory(history) + + // Should only contain: Summary2, and messages after it + expect(effectiveHistory.length).toBe(3) + expect(effectiveHistory[0].isSummary).toBe(true) + expect(effectiveHistory[0].condenseId).toBe(condenseId2) // Latest summary + expect(effectiveHistory[1].content).toBe("Database added") + expect(effectiveHistory[2].content).toBe("Now test it") + + // Verify NO condensed messages are included + const hasCondensedMessages = effectiveHistory.some( + (msg) => msg.condenseParent && history.some((m) => m.isSummary && m.condenseId === msg.condenseParent), + ) + expect(hasCondensedMessages).toBe(false) + + // Step 2: Get messages since last summary (on effective history) + const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory) + + // Should be the same as effective history since Summary2 is already at the start + expect(messagesSinceLastSummary.length).toBe(3) + expect(messagesSinceLastSummary[0].isSummary).toBe(true) + expect(messagesSinceLastSummary[0].condenseId).toBe(condenseId2) + + // CRITICAL: No previous history (Summary1 or original task) should be included + const hasSummary1 = messagesSinceLastSummary.some((m) => m.condenseId === condenseId1) + expect(hasSummary1).toBe(false) + + const hasOriginalTask = messagesSinceLastSummary.some((m) => m.content === "Build an app") + expect(hasOriginalTask).toBe(false) + }) + + it("should handle triple nested condense correctly", () => { + const condenseId1 = "condense-1" + const condenseId2 = "condense-2" + const condenseId3 = "condense-3" + + const history: ApiMessage[] = [ + // First condense content + { role: "user", content: "Task", ts: 100, condenseParent: condenseId1 }, + { + role: "user", + content: [{ type: "text", text: "## Summary 1" }], + ts: 199, + isSummary: true, + condenseId: condenseId1, + condenseParent: condenseId2, + }, + // Second condense content + { role: "assistant", content: "After S1", ts: 200, condenseParent: condenseId2 }, + { + role: "user", + content: [{ type: "text", text: "## Summary 2" }], + ts: 299, + isSummary: true, + condenseId: condenseId2, + condenseParent: condenseId3, + }, + // Third condense content + { role: "assistant", content: "After S2", ts: 300, condenseParent: condenseId3 }, + { + role: "user", + content: [{ type: "text", text: "## Summary 3" }], + ts: 399, + isSummary: true, + condenseId: condenseId3, + }, + // Current messages + { role: "assistant", content: "Current work", ts: 400 }, + ] + + const effectiveHistory = getEffectiveApiHistory(history) + + // Should only contain Summary3 and current work + expect(effectiveHistory.length).toBe(2) + expect(effectiveHistory[0].condenseId).toBe(condenseId3) + expect(effectiveHistory[1].content).toBe("Current work") + + const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory) + expect(messagesSinceLastSummary.length).toBe(2) + + // No previous summaries should be included + const hasPreviousSummaries = messagesSinceLastSummary.some( + (m) => m.condenseId === condenseId1 || m.condenseId === condenseId2, + ) + expect(hasPreviousSummaries).toBe(false) + }) + }) + + describe("getMessagesSinceLastSummary behavior with full vs effective history", () => { + it("should return consistent results when called with full history vs effective history", () => { + const condenseId = "condense-1" + + const fullHistory: ApiMessage[] = [ + { role: "user", content: "Original task", ts: 100, condenseParent: condenseId }, + { role: "assistant", content: "Response", ts: 200, condenseParent: condenseId }, + { + role: "user", + content: [{ type: "text", text: "Summary" }], + ts: 299, + isSummary: true, + condenseId, + }, + { role: "assistant", content: "After summary", ts: 300 }, + ] + + // Called with FULL history (as in summarizeConversation) + const fromFullHistory = getMessagesSinceLastSummary(fullHistory) + + // Called with EFFECTIVE history (as in attemptApiRequest) + const effectiveHistory = getEffectiveApiHistory(fullHistory) + const fromEffectiveHistory = getMessagesSinceLastSummary(effectiveHistory) + + // Both should return the same messages when summary is user role + expect(fromFullHistory.length).toBe(fromEffectiveHistory.length) + + // Both should start with the summary + expect(fromFullHistory[0].isSummary).toBe(true) + expect(fromEffectiveHistory[0].isSummary).toBe(true) + }) + + it("should not include condensed original task in effective history", () => { + const condenseId1 = "condense-1" + const condenseId2 = "condense-2" + + // Scenario: Two nested condenses with user-role summaries + const fullHistory: ApiMessage[] = [ + { role: "user", content: "Original task - should NOT appear", ts: 100, condenseParent: condenseId1 }, + { role: "assistant", content: "Old response", ts: 200, condenseParent: condenseId1 }, + // First summary (user role, fresh-start model), then condensed again + { + role: "user", + content: [{ type: "text", text: "Summary 1" }], + ts: 299, + isSummary: true, + condenseId: condenseId1, + condenseParent: condenseId2, + }, + { role: "assistant", content: "After S1", ts: 300, condenseParent: condenseId2 }, + // Second summary (user role, fresh-start model) + { + role: "user", + content: [{ type: "text", text: "Summary 2" }], + ts: 399, + isSummary: true, + condenseId: condenseId2, + }, + { role: "assistant", content: "Current message", ts: 400 }, + ] + + const effectiveHistory = getEffectiveApiHistory(fullHistory) + expect(effectiveHistory.length).toBe(2) // Summary2 + Current message + + const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory) + + // The original task should NOT be included + const hasOriginalTask = messagesSinceLastSummary.some((m) => + typeof m.content === "string" + ? m.content.includes("Original task") + : JSON.stringify(m.content).includes("Original task"), + ) + expect(hasOriginalTask).toBe(false) + + // Summary1 should not be included (it was condensed) + const hasSummary1 = messagesSinceLastSummary.some((m) => m.condenseId === condenseId1) + expect(hasSummary1).toBe(false) + }) + }) +}) diff --git a/src/core/condense/__tests__/rewind-after-condense.spec.ts b/src/core/condense/__tests__/rewind-after-condense.spec.ts index 84fdb63ca8..068f49a857 100644 --- a/src/core/condense/__tests__/rewind-after-condense.spec.ts +++ b/src/core/condense/__tests__/rewind-after-condense.spec.ts @@ -83,7 +83,7 @@ describe("Rewind After Condense - Issue #8295", () => { const messages: ApiMessage[] = [ { role: "user", content: "First message", ts: 1 }, { role: "assistant", content: "First response", ts: 2, condenseParent: condenseId }, - { role: "assistant", content: "Summary", ts: 3, isSummary: true, condenseId }, + { role: "user", content: "Summary", ts: 3, isSummary: true, condenseId }, ] const cleaned = cleanupAfterTruncation(messages) @@ -97,7 +97,7 @@ describe("Rewind After Condense - Issue #8295", () => { const condenseId2 = "summary-2" const messages: ApiMessage[] = [ { role: "user", content: "Message 1", ts: 1, condenseParent: condenseId1 }, - { role: "assistant", content: "Summary 1", ts: 2, isSummary: true, condenseId: condenseId1 }, + { role: "user", content: "Summary 1", ts: 2, isSummary: true, condenseId: condenseId1 }, { role: "user", content: "Message 2", ts: 3, condenseParent: condenseId2 }, // Summary 2 is NOT present (was truncated) ] @@ -203,8 +203,8 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "user", content: "Start", ts: 1 }, { role: "assistant", content: "Response 1", ts: 2, condenseParent: condenseId }, { role: "user", content: "More", ts: 3, condenseParent: condenseId }, - { role: "assistant", content: "Summary", ts: 4, isSummary: true, condenseId }, - { role: "user", content: "After summary", ts: 5 }, + { role: "user", content: "Summary", ts: 4, isSummary: true, condenseId }, + { role: "assistant", content: "After summary", ts: 5 }, ] // Fresh start model: effective history is summary + messages after it @@ -250,7 +250,7 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "assistant", content: "Response 3", ts: 600, condenseParent: condenseId }, { role: "user", content: "Even more", ts: 700, condenseParent: condenseId }, // Summary gets ts = firstKeptTs - 1 = 999, which is unique - { role: "assistant", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, + { role: "user", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, // First kept message { role: "user", content: "First kept message", ts: firstKeptTs }, { role: "assistant", content: "Response to first kept", ts: 1100 }, @@ -283,9 +283,9 @@ describe("Rewind After Condense - Issue #8295", () => { const messages: ApiMessage[] = [ { role: "user", content: "Initial", ts: 1 }, - { role: "assistant", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, - { role: "user", content: "First kept message", ts: firstKeptTs }, - { role: "assistant", content: "Response", ts: 9 }, + { role: "user", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, + { role: "assistant", content: "First kept message", ts: firstKeptTs }, + { role: "user", content: "Response", ts: 9 }, ] // Look up by first kept message's timestamp @@ -330,7 +330,7 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "user", content: "Now the tests", ts: 700, condenseParent: condenseId }, // Summary inserted before first kept message { - role: "assistant", + role: "user", content: "Summary: Built API with validation, working on tests", ts: 799, // msg8.ts - 1 isSummary: true, @@ -345,12 +345,12 @@ describe("Rewind After Condense - Issue #8295", () => { const effective = getEffectiveApiHistory(storageAfterCondense) // Should send exactly 4 messages to LLM: - // 1. Summary (assistant) + // 1. Summary (user) // 2-4. Last 3 kept messages expect(effective.length).toBe(4) // Verify exact order and content - expect(effective[0].role).toBe("assistant") + expect(effective[0].role).toBe("user") expect(effective[0].isSummary).toBe(true) expect(effective[0].content).toBe("Summary: Built API with validation, working on tests") @@ -394,7 +394,7 @@ describe("Rewind After Condense - Issue #8295", () => { // First summary - now ALSO tagged with condenseId2 (from second condense) { - role: "assistant", + role: "user", content: "Summary1: Built auth and database", ts: 799, isSummary: true, @@ -416,7 +416,7 @@ describe("Rewind After Condense - Issue #8295", () => { // Second summary - inserted before the last 3 kept messages { - role: "assistant", + role: "user", content: "Summary2: App complete with auth, DB, API, validation, errors, logging. Now testing.", ts: 1799, // msg18.ts - 1 isSummary: true, @@ -432,12 +432,12 @@ describe("Rewind After Condense - Issue #8295", () => { const effective = getEffectiveApiHistory(storageAfterDoubleCondense) // Should send exactly 4 messages to LLM: - // 1. Summary2 (assistant) - the ACTIVE summary + // 1. Summary2 (user) - the ACTIVE summary // 2-4. Last 3 kept messages expect(effective.length).toBe(4) // Verify exact order and content - expect(effective[0].role).toBe("assistant") + expect(effective[0].role).toBe("user") expect(effective[0].isSummary).toBe(true) expect(effective[0].condenseId).toBe(condenseId2) // Must be the SECOND summary expect(effective[0].content).toContain("Summary2") @@ -477,7 +477,7 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "user", content: "Start task", ts: 100, condenseParent: condenseId }, { role: "assistant", content: "Response 1", ts: 200, condenseParent: condenseId }, { role: "user", content: "Continue", ts: 300, condenseParent: condenseId }, - { role: "assistant", content: "Summary text", ts: 399, isSummary: true, condenseId }, + { role: "user", content: "Summary text", ts: 399, isSummary: true, condenseId }, // Kept messages - should alternate properly { role: "assistant", content: "Response after summary", ts: 400 }, { role: "user", content: "User message", ts: 500 }, @@ -486,10 +486,9 @@ describe("Rewind After Condense - Issue #8295", () => { const effective = getEffectiveApiHistory(storage) - // Verify the sequence: assistant(summary), assistant, user, assistant - // Note: Having two assistant messages in a row (summary + next response) is valid - // because the summary replaces what would have been multiple messages - expect(effective[0].role).toBe("assistant") + // Verify the sequence: user(summary), assistant, user, assistant + // This is the fresh-start model with user-role summaries + expect(effective[0].role).toBe("user") expect(effective[0].isSummary).toBe(true) expect(effective[1].role).toBe("assistant") expect(effective[2].role).toBe("user") @@ -502,10 +501,10 @@ describe("Rewind After Condense - Issue #8295", () => { const storage: ApiMessage[] = [ { role: "user", content: "First", ts: 100, condenseParent: condenseId }, { role: "assistant", content: "Condensed", ts: 200, condenseParent: condenseId }, - { role: "assistant", content: "Summary", ts: 299, isSummary: true, condenseId }, - { role: "user", content: "Kept 1", ts: 300 }, - { role: "assistant", content: "Kept 2", ts: 400 }, - { role: "user", content: "Kept 3", ts: 500 }, + { role: "user", content: "Summary", ts: 299, isSummary: true, condenseId }, + { role: "assistant", content: "Kept 1", ts: 300 }, + { role: "user", content: "Kept 2", ts: 400 }, + { role: "assistant", content: "Kept 3", ts: 500 }, ] const effective = getEffectiveApiHistory(storage) diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 0c92087aff..313bfcebb6 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -372,38 +372,22 @@ ${commandBlocks} return { messages: newMessages, summary, cost, newContextTokens, condenseId } } -/* Returns the list of all messages since the last summary message, including the summary. Returns all messages if there is no summary. */ +/** + * Returns the list of all messages since the last summary message, including the summary. + * Returns all messages if there is no summary. + * + * Note: Summary messages are always created with role: "user" (fresh-start model), + * so the first message since the last summary is guaranteed to be a user message. + */ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[] { - let lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary) + const lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary) if (lastSummaryIndexReverse === -1) { return messages } const lastSummaryIndex = messages.length - lastSummaryIndexReverse - 1 - const messagesSinceSummary = messages.slice(lastSummaryIndex) - - // Bedrock requires the first message to be a user message. - // We preserve the original first message to maintain context. - // See https://github.com/RooCodeInc/Roo-Code/issues/4147 - if (messagesSinceSummary.length > 0 && messagesSinceSummary[0].role !== "user") { - // Get the original first message (should always be a user message with the task) - const originalFirstMessage = messages[0] - if (originalFirstMessage && originalFirstMessage.role === "user") { - // Use the original first message unchanged to maintain full context - return [originalFirstMessage, ...messagesSinceSummary] - } else { - // Fallback to generic message if no original first message exists (shouldn't happen) - const userMessage: ApiMessage = { - role: "user", - content: "Please continue from the following summary:", - ts: messages[0]?.ts ? messages[0].ts - 1 : Date.now(), - } - return [userMessage, ...messagesSinceSummary] - } - } - - return messagesSinceSummary + return messages.slice(lastSummaryIndex) } /** diff --git a/src/core/context-management/__tests__/context-management.spec.ts b/src/core/context-management/__tests__/context-management.spec.ts index 7c5db2d510..2616ea571d 100644 --- a/src/core/context-management/__tests__/context-management.spec.ts +++ b/src/core/context-management/__tests__/context-management.spec.ts @@ -578,8 +578,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, @@ -751,8 +751,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, @@ -899,8 +899,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, @@ -965,8 +965,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, diff --git a/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts index 541a710621..1af6b43bc1 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts @@ -267,7 +267,7 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 100, role: "user", content: "First message", condenseParent: condenseId }, { ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId }, { ts: 300, role: "user", content: "Second message", condenseParent: condenseId }, - { ts: 799, role: "assistant", content: "Summary", isSummary: true, condenseId }, + { ts: 799, role: "user", content: "Summary", isSummary: true, condenseId }, { ts: 800, role: "assistant", content: "Kept message 1" }, { ts: 900, role: "user", content: "Kept message 2" }, { ts: 1000, role: "assistant", content: "Kept message 3" }, @@ -314,8 +314,8 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 100, role: "user", content: "Task start", condenseParent: condenseId }, { ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId }, { ts: 300, role: "user", content: "Message 2", condenseParent: condenseId }, - { ts: 999, role: "assistant", content: "Summary", isSummary: true, condenseId }, - { ts: 1000, role: "user", content: "First kept" }, + { ts: 999, role: "user", content: "Summary", isSummary: true, condenseId }, + { ts: 1000, role: "assistant", content: "First kept" }, ] // Delete "Message 2" (ts=300) - this removes summary too, so orphaned tags should be cleared @@ -357,7 +357,7 @@ describe("webviewMessageHandler delete functionality", () => { // First summary - ALSO tagged with condenseId2 from second condense { ts: 799, - role: "assistant", + role: "user", content: "Summary1", isSummary: true, condenseId: condenseId1, @@ -367,7 +367,7 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 1000, role: "assistant", content: "Msg after summary1", condenseParent: condenseId2 }, { ts: 1100, role: "user", content: "More msgs", condenseParent: condenseId2 }, // Second summary - { ts: 1799, role: "assistant", content: "Summary2", isSummary: true, condenseId: condenseId2 }, + { ts: 1799, role: "user", content: "Summary2", isSummary: true, condenseId: condenseId2 }, // Kept messages { ts: 1800, role: "user", content: "Kept1" }, { ts: 1900, role: "assistant", content: "Kept2" }, @@ -407,9 +407,9 @@ describe("webviewMessageHandler delete functionality", () => { // Summary and regular message share timestamp (edge case) getCurrentTaskMock.apiConversationHistory = [ { ts: 900, role: "user", content: "Previous message" }, - { ts: sharedTs, role: "assistant", content: "Summary", isSummary: true, condenseId: "abc" }, - { ts: sharedTs, role: "user", content: "First kept message" }, - { ts: 1100, role: "assistant", content: "Response" }, + { ts: sharedTs, role: "user", content: "Summary", isSummary: true, condenseId: "abc" }, + { ts: sharedTs, role: "assistant", content: "First kept message" }, + { ts: 1100, role: "user", content: "Response" }, ] // Delete at shared timestamp - MessageManager uses ts < cutoffTs, so ALL @@ -450,10 +450,10 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 100, role: "user", content: "Task start", condenseParent: condenseId }, { ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId }, // Summary timestamp is BEFORE the kept messages (this is the bug scenario) - { ts: 299, role: "assistant", content: "Summary text", isSummary: true, condenseId }, - { ts: 300, role: "user", content: "Message to delete this and after" }, - { ts: 400, role: "assistant", content: "Response 2" }, - { ts: 600, role: "user", content: "Post-condense message" }, + { ts: 299, role: "user", content: "Summary text", isSummary: true, condenseId }, + { ts: 300, role: "assistant", content: "Message to delete this and after" }, + { ts: 400, role: "user", content: "Response 2" }, + { ts: 600, role: "assistant", content: "Post-condense message" }, ] // Delete at ts=300 - this removes condense_context (ts=500), so Summary should be removed too @@ -510,30 +510,30 @@ describe("webviewMessageHandler delete functionality", () => { // First summary (also tagged with condenseId2 from second condense) { ts: 799, - role: "assistant", + role: "user", content: "First summary", isSummary: true, condenseId: condenseId1, condenseParent: condenseId2, }, - { ts: 900, role: "user", content: "After first condense", condenseParent: condenseId2 }, + { ts: 900, role: "assistant", content: "After first condense", condenseParent: condenseId2 }, { ts: 1000, - role: "assistant", + role: "user", content: "Response after 1st condense", condenseParent: condenseId2, }, - { ts: 1100, role: "user", content: "Message to delete this and after" }, + { ts: 1100, role: "assistant", content: "Message to delete this and after" }, // Second summary (timestamp is BEFORE the messages it summarized for sort purposes) { ts: 1799, - role: "assistant", + role: "user", content: "Second summary", isSummary: true, condenseId: condenseId2, }, - { ts: 1900, role: "user", content: "Post second condense" }, - { ts: 2000, role: "assistant", content: "Final response" }, + { ts: 1900, role: "assistant", content: "Post second condense" }, + { ts: 2000, role: "user", content: "Final response" }, ] // Delete at ts=1100 - this removes second condense_context (ts=1800) but keeps first (ts=800) From 2391a0f06584b6925e4071826112e8caacc11975 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:10:23 -0500 Subject: [PATCH 006/256] fix: use --force by default when deleting worktrees (#10986) Co-authored-by: Roo Code --- .../components/worktrees/DeleteWorktreeModal.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx index 3f40c73ff6..9e3f4802d5 100644 --- a/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx +++ b/webview-ui/src/components/worktrees/DeleteWorktreeModal.tsx @@ -18,7 +18,7 @@ export const DeleteWorktreeModal = ({ open, onClose, worktree, onSuccess }: Dele const { t } = useAppTranslation() const [isDeleting, setIsDeleting] = useState(false) - const [forceDelete, setForceDelete] = useState(false) + const [forceDeleteLocked, setForceDeleteLocked] = useState(false) const [error, setError] = useState(null) useEffect(() => { @@ -44,12 +44,15 @@ export const DeleteWorktreeModal = ({ open, onClose, worktree, onSuccess }: Dele setError(null) setIsDeleting(true) + // Always force delete unless worktree is locked and user hasn't opted in + const shouldForce = worktree.isLocked ? forceDeleteLocked : true + vscode.postMessage({ type: "deleteWorktree", worktreePath: worktree.path, - worktreeForce: forceDelete, + worktreeForce: shouldForce, }) - }, [worktree.path, forceDelete]) + }, [worktree.path, worktree.isLocked, forceDeleteLocked]) return ( !isOpen && onClose()}> @@ -90,13 +93,13 @@ export const DeleteWorktreeModal = ({ open, onClose, worktree, onSuccess }: Dele - {/* Force delete option (if worktree is locked) */} + {/* Force delete option (only shown if worktree is locked) */} {worktree.isLocked && (
setForceDelete(checked === true)} + checked={forceDeleteLocked} + onCheckedChange={(checked) => setForceDeleteLocked(checked === true)} />
+ label={t("settings:terminal.outputPreviewSize.label")}> -
- setCachedStateField("terminalOutputLineLimit", value)} - data-testid="terminal-output-limit-slider" - /> - {terminalOutputLineLimit ?? 500} -
+
- - - {" "} - - -
-
- - -
- - setCachedStateField("terminalOutputCharacterLimit", value) - } - data-testid="terminal-output-character-limit-slider" - /> - {terminalOutputCharacterLimit ?? 50000} -
-
- - - {" "} - - -
-
- - - setCachedStateField("terminalCompressProgressBar", e.target.checked) - } - data-testid="terminal-compress-progress-bar-checkbox"> - {t("settings:terminal.compressProgressBar.label")} - -
- - - {" "} - - + {t("settings:terminal.outputPreviewSize.description")}
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx index ddaf6a7b99..89be961625 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx @@ -193,7 +193,6 @@ describe("SettingsView - Change Detection Fix", () => { maxReadFileLine: -1, maxImageFileSize: 5, maxTotalImageSize: 20, - terminalCompressProgressBar: false, maxConcurrentFileReads: 5, customCondensingPrompt: "", customSupportPrompts: {}, diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index 4a1733c376..996dad8639 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -198,7 +198,6 @@ describe("SettingsView - Unsaved Changes Detection", () => { maxReadFileLine: -1, maxImageFileSize: 5, maxTotalImageSize: 20, - terminalCompressProgressBar: false, maxConcurrentFileReads: 5, customCondensingPrompt: "", customSupportPrompts: {}, diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 26b3851c1b..d37f09bbc5 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -96,10 +96,8 @@ export interface ExtensionStateContextType extends ExtensionState { setWriteDelayMs: (value: number) => void screenshotQuality?: number setScreenshotQuality: (value: number) => void - terminalOutputLineLimit?: number - setTerminalOutputLineLimit: (value: number) => void - terminalOutputCharacterLimit?: number - setTerminalOutputCharacterLimit: (value: number) => void + terminalOutputPreviewSize?: "small" | "medium" | "large" + setTerminalOutputPreviewSize: (value: "small" | "medium" | "large") => void mcpEnabled: boolean setMcpEnabled: (value: boolean) => void enableMcpServerCreation: boolean @@ -140,8 +138,6 @@ export interface ExtensionStateContextType extends ExtensionState { pinnedApiConfigs?: Record setPinnedApiConfigs: (value: Record) => void togglePinnedApiConfig: (configName: string) => void - terminalCompressProgressBar?: boolean - setTerminalCompressProgressBar: (value: boolean) => void setHistoryPreviewCollapsed: (value: boolean) => void setReasoningBlockCollapsed: (value: boolean) => void enterBehavior?: "send" | "newline" @@ -213,8 +209,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode writeDelayMs: 1000, browserViewportSize: "900x600", screenshotQuality: 75, - terminalOutputLineLimit: 500, - terminalOutputCharacterLimit: 50000, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, enableMcpServerCreation: false, @@ -247,7 +241,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode maxConcurrentFileReads: 5, // Default concurrent file reads terminalZshP10k: false, // Default Powerlevel10k integration setting terminalZdotdir: false, // Default ZDOTDIR handling setting - terminalCompressProgressBar: true, // Default to compress progress bar output historyPreviewCollapsed: false, // Initialize the new state (default to expanded) reasoningBlockCollapsed: true, // Default to collapsed enterBehavior: "send", // Default: Enter sends, Shift+Enter creates newline @@ -544,10 +537,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => ({ ...prevState, browserViewportSize: value })), setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })), setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })), - setTerminalOutputLineLimit: (value) => - setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })), - setTerminalOutputCharacterLimit: (value) => - setState((prevState) => ({ ...prevState, terminalOutputCharacterLimit: value })), + setTerminalOutputPreviewSize: (value) => + setState((prevState) => ({ ...prevState, terminalOutputPreviewSize: value })), setTerminalShellIntegrationTimeout: (value) => setState((prevState) => ({ ...prevState, terminalShellIntegrationTimeout: value })), setTerminalShellIntegrationDisabled: (value) => @@ -581,8 +572,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setMaxImageFileSize: (value) => setState((prevState) => ({ ...prevState, maxImageFileSize: value })), setMaxTotalImageSize: (value) => setState((prevState) => ({ ...prevState, maxTotalImageSize: value })), setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })), - setTerminalCompressProgressBar: (value) => - setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })), togglePinnedApiConfig: (configId) => setState((prevState) => { const currentPinned = prevState.pinnedApiConfigs || {} diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index a00728e481..c44a872433 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} servidors MCP", "messageTemplate": "Tens {{tools}} habilitades via {{servers}}. Un nombre tant alt pot confondre el model i portar a errors. Intenta mantenir-lo per sota de {{threshold}}.", "openMcpSettings": "Obrir configuració de MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 1509137a90..dfe769e6ea 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -724,6 +724,15 @@ "label": "Límit de caràcters del terminal", "description": "Anul·la el límit de línies per evitar problemes de memòria imposant un límit dur a la mida de sortida. Si se supera, manté l'inici i el final i mostra un marcador a Roo on s'ha omès el contingut. <0>Aprèn-ne més" }, + "outputPreviewSize": { + "label": "Mida de la previsualització de la sortida d'ordres", + "description": "Controla quanta sortida d'ordres veu Roo directament. La sortida completa sempre es desa i és accessible quan calgui.", + "options": { + "small": "Petita (5KB)", + "medium": "Mitjana (10KB)", + "large": "Gran (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Temps d'espera d'integració del shell del terminal", "description": "Quant de temps esperar la integració del shell de VS Code abans d'executar comandes. Augmenta si el teu shell s'inicia lentament o veus errors 'Integració del Shell No Disponible'. <0>Aprèn-ne més" @@ -736,10 +745,6 @@ "label": "Retard de comanda del terminal", "description": "Afegeix una pausa breu després de cada comanda perquè el terminal de VS Code pugui buidar tota la sortida (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Usa només si veus que falta sortida final; altrament deixa a 0. <0>Aprèn-ne més" }, - "compressProgressBar": { - "label": "Comprimeix sortida de barra de progrés", - "description": "Col·lapsa barres de progrés/spinners perquè només es mantingui l'estat final (estalvia tokens). <0>Aprèn-ne més" - }, "powershellCounter": { "label": "Activa solució de comptador de PowerShell", "description": "Activa quan falta o es duplica la sortida de PowerShell; afegeix un petit comptador a cada comanda per estabilitzar la sortida. Mantén desactivat si la sortida ja es veu correcta. <0>Aprèn-ne més" diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 90c68683c5..1f3f11bc81 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} MCP-Server", "messageTemplate": "Du hast {{tools}} über {{servers}} aktiviert. Eine so hohe Anzahl kann das Modell verwirren und zu Fehlern führen. Versuche, es unter {{threshold}} zu halten.", "openMcpSettings": "MCP-Einstellungen öffnen" + }, + "readCommandOutput": { + "title": "Roo las Befehlsausgabe" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index bc275a64e5..c49fac0f3b 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -724,6 +724,15 @@ "label": "Terminal-Zeichenlimit", "description": "Überschreibt das Zeilenlimit, um Speicherprobleme durch eine harte Obergrenze für die Ausgabegröße zu vermeiden. Bei Überschreitung behält es Anfang und Ende und zeigt Roo einen Platzhalter, wo Inhalt übersprungen wird. <0>Mehr erfahren" }, + "outputPreviewSize": { + "label": "Befehlsausgabe-Vorschaugröße", + "description": "Steuert, wie viel Befehlsausgabe Roo direkt sieht. Die vollständige Ausgabe wird immer gespeichert und ist bei Bedarf zugänglich.", + "options": { + "small": "Klein (5KB)", + "medium": "Mittel (10KB)", + "large": "Groß (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Terminal-Shell-Integrations-Timeout", "description": "Wie lange auf VS Code Shell-Integration gewartet wird, bevor Befehle ausgeführt werden. Erhöhe den Wert, wenn deine Shell langsam startet oder du 'Shell-Integration nicht verfügbar'-Fehler siehst. <0>Mehr erfahren" @@ -736,10 +745,6 @@ "label": "Terminal-Befehlsverzögerung", "description": "Fügt nach jedem Befehl eine kurze Pause hinzu, damit das VS Code-Terminal alle Ausgaben leeren kann (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Verwende dies nur, wenn du fehlende Tail-Ausgabe siehst; sonst lass es bei 0. <0>Mehr erfahren" }, - "compressProgressBar": { - "label": "Fortschrittsbalken-Ausgabe komprimieren", - "description": "Klappt Fortschrittsbalken/Spinner zusammen, sodass nur der Endzustand erhalten bleibt (spart Token). <0>Mehr erfahren" - }, "powershellCounter": { "label": "PowerShell-Zähler-Workaround aktivieren", "description": "Schalte dies ein, wenn PowerShell-Ausgabe fehlt oder dupliziert wird; es fügt jedem Befehl einen kleinen Zähler hinzu, um die Ausgabe zu stabilisieren. Lass es ausgeschaltet, wenn die Ausgabe bereits korrekt aussieht. <0>Mehr erfahren" diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index f585d88261..d167a19ff3 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -495,5 +495,8 @@ "serversPart_other": "{{count}} MCP servers", "messageTemplate": "You have {{tools}} enabled via {{servers}}. Such a high number can confuse the model and lead to errors. Try to keep it below {{threshold}}.", "openMcpSettings": "Open MCP Settings" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 7045ef07d1..63f4056d66 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -733,6 +733,15 @@ "label": "Terminal character limit", "description": "Overrides the line limit to prevent memory issues by enforcing a hard cap on output size. If exceeded, keeps the beginning and end and shows a placeholder to Roo where content is skipped. <0>Learn more" }, + "outputPreviewSize": { + "label": "Command output preview size", + "description": "Controls how much command output Roo sees directly. Full output is always saved and accessible when needed.", + "options": { + "small": "Small (5KB)", + "medium": "Medium (10KB)", + "large": "Large (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Terminal shell integration timeout", "description": "How long to wait for VS Code shell integration before running commands. Raise if your shell starts slowly or you see 'Shell Integration Unavailable' errors. <0>Learn more" @@ -745,10 +754,6 @@ "label": "Terminal command delay", "description": "Adds a short pause after each command so the VS Code terminal can flush all output (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Use only if you see missing tail output; otherwise leave at 0. <0>Learn more" }, - "compressProgressBar": { - "label": "Compress progress bar output", - "description": "Collapses progress bars/spinners so only the final state is kept (saves tokens). <0>Learn more" - }, "powershellCounter": { "label": "Enable PowerShell counter workaround", "description": "Turn this on when PowerShell output is missing or duplicated; it appends a tiny counter to each command to stabilize output. Keep this off if output already looks correct. <0>Learn more" diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 3140b7834b..2c9418cfa7 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} servidores MCP", "messageTemplate": "Tienes {{tools}} habilitadas a través de {{servers}}. Un número tan alto puede confundir al modelo y llevar a errores. Intenta mantenerlo por debajo de {{threshold}}.", "openMcpSettings": "Abrir configuración de MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 36243b99be..7115da6795 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -724,6 +724,15 @@ "label": "Límite de caracteres del terminal", "description": "Anula el límite de líneas para evitar problemas de memoria imponiendo un límite estricto al tamaño de salida. Si se excede, mantiene el inicio y el final y muestra un marcador a Roo donde se omite el contenido. <0>Más información" }, + "outputPreviewSize": { + "label": "Tamaño de vista previa de salida de comandos", + "description": "Controla cuánta salida de comandos ve Roo directamente. La salida completa siempre se guarda y es accesible cuando sea necesario.", + "options": { + "small": "Pequeño (5KB)", + "medium": "Mediano (10KB)", + "large": "Grande (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Tiempo de espera de integración del shell del terminal", "description": "Cuánto tiempo esperar la integración del shell de VS Code antes de ejecutar comandos. Aumenta si tu shell inicia lentamente o ves errores 'Integración del Shell No Disponible'. <0>Más información" @@ -736,10 +745,6 @@ "label": "Retraso de comando del terminal", "description": "Añade una pausa breve después de cada comando para que el terminal de VS Code pueda vaciar toda la salida (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Usa solo si ves salida final faltante; si no, deja en 0. <0>Más información" }, - "compressProgressBar": { - "label": "Comprimir salida de barra de progreso", - "description": "Colapsa barras de progreso/spinners para que solo se mantenga el estado final (ahorra tokens). <0>Más información" - }, "powershellCounter": { "label": "Activar solución del contador de PowerShell", "description": "Activa cuando falta o se duplica la salida de PowerShell; añade un pequeño contador a cada comando para estabilizar la salida. Mantén desactivado si la salida ya se ve correcta. <0>Más información" diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index ec8c68c521..8aa09075dc 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} serveurs MCP", "messageTemplate": "Tu as {{tools}} activés via {{servers}}. Un nombre aussi élevé peut confondre le modèle et entraîner des erreurs. Essaie de le maintenir en dessous de {{threshold}}.", "openMcpSettings": "Ouvrir les paramètres MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 1c28b76307..8cdbc1edb4 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -724,6 +724,15 @@ "label": "Limite de caractères du terminal", "description": "Remplace la limite de lignes pour éviter les problèmes de mémoire en imposant un plafond strict sur la taille de sortie. Si dépassé, conserve le début et la fin et affiche un espace réservé à Roo là où le contenu est ignoré. <0>En savoir plus" }, + "outputPreviewSize": { + "label": "Taille de l'aperçu de sortie des commandes", + "description": "Contrôle la quantité de sortie de commande que Roo voit directement. La sortie complète est toujours sauvegardée et accessible en cas de besoin.", + "options": { + "small": "Petite (5KB)", + "medium": "Moyenne (10KB)", + "large": "Grande (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Délai d'attente d'intégration du shell du terminal", "description": "Temps d'attente de l'intégration du shell de VS Code avant d'exécuter des commandes. Augmentez si votre shell démarre lentement ou si vous voyez des erreurs 'Intégration du Shell Indisponible'. <0>En savoir plus" @@ -736,10 +745,6 @@ "label": "Délai de commande du terminal", "description": "Ajoute une courte pause après chaque commande pour que le terminal VS Code puisse vider toute la sortie (bash/zsh : PROMPT_COMMAND sleep ; PowerShell : start-sleep). Utilisez uniquement si vous voyez une sortie de fin manquante ; sinon laissez à 0. <0>En savoir plus" }, - "compressProgressBar": { - "label": "Compresser la sortie de barre de progression", - "description": "Réduit les barres de progression/spinners pour ne conserver que l'état final (économise des jetons). <0>En savoir plus" - }, "powershellCounter": { "label": "Activer la solution de contournement du compteur PowerShell", "description": "Activez lorsque la sortie PowerShell est manquante ou dupliquée ; ajoute un petit compteur à chaque commande pour stabiliser la sortie. Laissez désactivé si la sortie semble déjà correcte. <0>En savoir plus" diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index bf6556f7f7..9c155e62ec 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} MCP सर्वर", "messageTemplate": "आपके पास {{servers}} के माध्यम से {{tools}} सक्षम हैं। इतनी अधिक संख्या मॉडल को भ्रमित कर सकती है और त्रुटियों का कारण बन सकती है। इसे {{threshold}} से नीचे रखने का प्रयास करें।", "openMcpSettings": "MCP सेटिंग्स खोलें" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 4974ff706b..3f7e0dfa9a 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -725,6 +725,15 @@ "label": "टर्मिनल वर्ण सीमा", "description": "मेमोरी समस्याओं को रोकने के लिए आउटपुट आकार पर कठोर सीमा लगाकर लाइन सीमा को ओवरराइड करता है। यदि पार हो जाती है, तो शुरुआत और अंत रखता है और Roo को प्लेसहोल्डर दिखाता है जहां सामग्री छोड़ी गई है। <0>अधिक जानें" }, + "outputPreviewSize": { + "label": "कमांड आउटपुट पूर्वावलोकन आकार", + "description": "नियंत्रित करता है कि Roo कितना कमांड आउटपुट सीधे देखता है। पूर्ण आउटपुट हमेशा सहेजा जाता है और आवश्यकता पड़ने पर सुलभ होता है।", + "options": { + "small": "छोटा (5KB)", + "medium": "मध्यम (10KB)", + "large": "बड़ा (20KB)" + } + }, "shellIntegrationTimeout": { "label": "टर्मिनल शेल एकीकरण टाइमआउट", "description": "कमांड चलाने से पहले VS Code शेल एकीकरण की प्रतीक्षा करने का समय। यदि आपका शेल धीरे शुरू होता है या आप 'Shell Integration Unavailable' त्रुटियां देखते हैं तो बढ़ाएं। <0>अधिक जानें" @@ -737,10 +746,6 @@ "label": "टर्मिनल कमांड विलंब", "description": "प्रत्येक कमांड के बाद छोटा विराम जोड़ता है ताकि VS Code टर्मिनल सभी आउटपुट फ्लश कर सके (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep)। केवल तभी उपयोग करें जब टेल आउटपुट गायब हो; अन्यथा 0 पर छोड़ दें। <0>अधिक जानें" }, - "compressProgressBar": { - "label": "प्रगति बार आउटपुट संपीड़ित करें", - "description": "प्रगति बार/स्पिनर को संक्षिप्त करता है ताकि केवल अंतिम स्थिति रखी जाए (token बचाता है)। <0>अधिक जानें" - }, "powershellCounter": { "label": "PowerShell काउंटर समाधान सक्षम करें", "description": "जब PowerShell आउटपुट गायब हो या डुप्लिकेट हो तो इसे चालू करें; यह आउटपुट को स्थिर करने के लिए प्रत्येक कमांड में एक छोटा काउंटर जोड़ता है। यदि आउटपुट पहले से सही दिखता है तो इसे बंद रखें। <0>अधिक जानें" diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 9695949df6..c8569f3646 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -510,5 +510,8 @@ "serversPart_other": "{{count}} server MCP", "messageTemplate": "Anda memiliki {{tools}} diaktifkan melalui {{servers}}. Jumlah yang begitu besar dapat membingungkan model dan menyebabkan kesalahan. Cobalah untuk menjaganya di bawah {{threshold}}.", "openMcpSettings": "Buka Pengaturan MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 908c975a5b..c1505c4ba9 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -729,6 +729,15 @@ "label": "Batas karakter terminal", "description": "Override batas baris untuk mencegah masalah memori dengan memberlakukan cap keras pada ukuran output. Jika terlampaui, simpan awal dan akhir lalu tampilkan placeholder ke Roo di mana konten dilewati. <0>Pelajari lebih lanjut" }, + "outputPreviewSize": { + "label": "Ukuran pratinjau keluaran perintah", + "description": "Mengontrol seberapa banyak keluaran perintah yang dilihat Roo secara langsung. Keluaran lengkap selalu disimpan dan dapat diakses saat diperlukan.", + "options": { + "small": "Kecil (5KB)", + "medium": "Sedang (10KB)", + "large": "Besar (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Timeout integrasi shell terminal", "description": "Waktu tunggu integrasi shell VS Code sebelum menjalankan perintah. Naikkan jika shell lambat start atau muncul error 'Shell Integration Unavailable'. <0>Pelajari lebih lanjut" @@ -741,10 +750,6 @@ "label": "Delay perintah terminal", "description": "Tambahkan jeda singkat setelah setiap perintah agar VS Code terminal bisa flush semua output (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Gunakan hanya jika output ekor hilang; jika tidak biarkan di 0. <0>Pelajari lebih lanjut" }, - "compressProgressBar": { - "label": "Kompres keluaran bilah kemajuan", - "description": "Menciutkan bilah kemajuan/spinner sehingga hanya status akhir yang disimpan (menghemat token). <0>Pelajari lebih lanjut" - }, "powershellCounter": { "label": "Aktifkan solusi penghitung PowerShell", "description": "Aktifkan saat keluaran PowerShell hilang atau digandakan; menambahkan penghitung kecil ke setiap perintah untuk menstabilkan keluaran. Biarkan nonaktif jika keluaran sudah terlihat benar. <0>Pelajari lebih lanjut" diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 6926a0b221..ac00a6dea0 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} server MCP", "messageTemplate": "Hai {{tools}} abilitate via {{servers}}. Un numero così alto può confondere il modello e portare a errori. Prova a mantenerlo sotto {{threshold}}.", "openMcpSettings": "Apri impostazioni MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 1c3c7e494d..139de3f16d 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -725,6 +725,15 @@ "label": "Limite caratteri terminale", "description": "Sovrascrive il limite di righe per prevenire problemi di memoria imponendo un limite rigido alla dimensione di output. Se superato, mantiene l'inizio e la fine e mostra un segnaposto a Roo dove il contenuto viene saltato. <0>Scopri di più" }, + "outputPreviewSize": { + "label": "Dimensione anteprima output comandi", + "description": "Controlla quanto output dei comandi Roo vede direttamente. L'output completo viene sempre salvato ed è accessibile quando necessario.", + "options": { + "small": "Piccola (5KB)", + "medium": "Media (10KB)", + "large": "Grande (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Timeout integrazione shell terminale", "description": "Quanto tempo attendere l'integrazione della shell di VS Code prima di eseguire i comandi. Aumenta se la tua shell si avvia lentamente o vedi errori 'Integrazione Shell Non Disponibile'. <0>Scopri di più" @@ -737,10 +746,6 @@ "label": "Ritardo comando terminale", "description": "Aggiunge una breve pausa dopo ogni comando affinché il terminale VS Code possa svuotare tutto l'output (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Usa solo se vedi output finale mancante; altrimenti lascia a 0. <0>Scopri di più" }, - "compressProgressBar": { - "label": "Comprimi output barra di avanzamento", - "description": "Comprime barre di avanzamento/spinner in modo che venga mantenuto solo lo stato finale (risparmia token). <0>Scopri di più" - }, "powershellCounter": { "label": "Abilita workaround contatore PowerShell", "description": "Attiva quando l'output PowerShell è mancante o duplicato; aggiunge un piccolo contatore a ogni comando per stabilizzare l'output. Mantieni disattivato se l'output sembra già corretto. <0>Scopri di più" diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 011f242969..34a494ba23 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} MCP サーバー", "messageTemplate": "{{servers}}経由で{{tools}}が有効になっています。このような高い数は、モデルを混乱させてエラーを引き起こす可能性があります。{{threshold}}以下に保つようにしてください。", "openMcpSettings": "MCP 設定を開く" + }, + "readCommandOutput": { + "title": "Rooがコマンド出力を読み込みました" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b4af9d4033..7ed6498351 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -725,6 +725,15 @@ "label": "ターミナル文字制限", "description": "出力サイズにハードキャップを適用してメモリ問題を防ぐため、行制限を上書きします。超過した場合、最初と最後を保持し、コンテンツがスキップされた箇所にRooにプレースホルダーを表示します。<0>詳細情報" }, + "outputPreviewSize": { + "label": "コマンド出力プレビューサイズ", + "description": "Rooが直接確認できるコマンド出力の量を制御します。完全な出力は常に保存され、必要に応じてアクセス可能です。", + "options": { + "small": "小 (5KB)", + "medium": "中 (10KB)", + "large": "大 (20KB)" + } + }, "shellIntegrationTimeout": { "label": "ターミナルシェル統合タイムアウト", "description": "コマンドを実行する前�����VS Codeシェル統合を待機する時間。シェルが遅く起動する場合や「シェル統合が利用できません」というエラーが表示される場合は、この値を増やしてください。<0>詳細" @@ -737,10 +746,6 @@ "label": "ターミナルコマンド遅延", "description": "VS Codeターミナルがすべての出力をフラッシュできるよう、各コマンド後に短い一時停止を追加します(bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep)。末尾出力が欠落している場合のみ使用;それ以外は0のままにします。<0>詳細情報" }, - "compressProgressBar": { - "label": "プログレスバー出力を圧���������", - "description": "プログレスバー/スピナーを折りたたんで、最終状態のみを保持します(トークンを節約します)。<0>詳細情報" - }, "powershellCounter": { "label": "PowerShellカウンターの回避策を有効にする", "description": "PowerShellの出力が欠落または重複している場合にこれをオンにします。出力を安定させるために各コマンドに小さなカウンターを追加します。出力がすでに正しい場合はオフのままにします。<0>詳細情報" diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 4367fce1c4..18d0089e34 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}}개 MCP 서버", "messageTemplate": "{{servers}}를 통해 {{tools}}가 활성화되어 있습니다. 이렇게 많은 수의 도구는 모델을 혼동시키고 오류를 유발할 수 있습니다. {{threshold}} 이하로 유지하도록 노력하세요.", "openMcpSettings": "MCP 설정 열기" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 2888d75bb0..20bf3858f7 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -725,6 +725,15 @@ "label": "터미널 문자 제한", "description": "출력 크기에 대한 엄격한 상한을 적용하여 메모리 문제를 방지하기 위해 줄 제한을 재정의합니다. 초과하면 시작과 끝을 유지하고 내용이 생략된 곳에 Roo에게 자리 표시자를 표시합니다. <0>자세히 알아보기" }, + "outputPreviewSize": { + "label": "명령 출력 미리보기 크기", + "description": "Roo가 직접 보는 명령 출력량을 제어합니다. 전체 출력은 항상 저장되며 필요할 때 액세스할 수 있습니다.", + "options": { + "small": "작게 (5KB)", + "medium": "보통 (10KB)", + "large": "크게 (20KB)" + } + }, "shellIntegrationTimeout": { "label": "터미널 셸 통합 시간 초과", "description": "명령을 실행하기 전에 VS Code 셸 통합을 기다리는 시간입니다. 셸이 느리게 시작되거나 '셸 통합을 사용할 수 없음' 오류가 표시되면 이 값을 늘리십시오. <0>자세히 알아보기" @@ -737,10 +746,6 @@ "label": "터미널 명령 지연", "description": "VS Code 터미널이 모든 출력을 플러시할 수 있도록 각 명령 후에 짧은 일시 중지를 추가합니다(bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). 누락된 꼬리 출력이 표시되는 경우에만 사용하고, 그렇지 않으면 0으로 둡니다. <0>자세히 알아보기" }, - "compressProgressBar": { - "label": "진행률 표시줄 출력 압축", - "description": "진행률 표시줄/스피너를 축소하여 최종 상태만 유지합니다(토큰 절약). <0>자세히 알아보기" - }, "powershellCounter": { "label": "PowerShell 카운터 해결 방법 활성화", "description": "PowerShell 출력이 누락되거나 중복될 때 이 기능을 켜십시오. 출력을 안정화하기 위해 각 명령에 작은 카운터를 추가합니다. 출력이 이미 올바르게 표시되면 이 기능을 끄십시오. <0>자세히 알아보기" diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index a889217ef9..5f0f693619 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} MCP servers", "messageTemplate": "Je hebt {{tools}} ingeschakeld via {{servers}}. Zoveel tools kunnen het model verwarren en tot fouten leiden. Probeer dit onder {{threshold}} te houden.", "openMcpSettings": "MCP-instellingen openen" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 83e1f4b7ab..25b2a48f64 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -725,6 +725,15 @@ "label": "Terminal-tekenlimiet", "description": "Overschrijft de regellimiet om geheugenproblemen te voorkomen door een harde limiet op uitvoergrootte af te dwingen. Bij overschrijding behoudt het begin en einde en toont een placeholder aan Roo waar inhoud wordt overgeslagen. <0>Meer informatie" }, + "outputPreviewSize": { + "label": "Grootte opdrachtuitvoer voorvertoning", + "description": "Bepaalt hoeveel opdrachtuitvoer Roo direct ziet. Volledige uitvoer wordt altijd opgeslagen en is toegankelijk wanneer nodig.", + "options": { + "small": "Klein (5KB)", + "medium": "Gemiddeld (10KB)", + "large": "Groot (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Terminal-shell-integratie timeout", "description": "Hoe lang te wachten op VS Code-shell-integratie voordat commando's worden uitgevoerd. Verhoog als je shell traag opstart of je 'Shell-Integratie Niet Beschikbaar'-fouten ziet. <0>Meer informatie" @@ -737,10 +746,6 @@ "label": "Terminal-commandovertraging", "description": "Voegt korte pauze toe na elk commando zodat VS Code-terminal alle uitvoer kan flushen (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Gebruik alleen als je ontbrekende tail-uitvoer ziet; anders op 0 laten. <0>Meer informatie" }, - "compressProgressBar": { - "label": "Voortgangsbalk-uitvoer comprimeren", - "description": "Klapt voortgangsbalken/spinners in zodat alleen eindstatus behouden blijft (bespaart tokens). <0>Meer informatie" - }, "powershellCounter": { "label": "PowerShell-teller workaround inschakelen", "description": "Schakel in wanneer PowerShell-uitvoer ontbreekt of gedupliceerd wordt; voegt kleine teller toe aan elk commando om uitvoer te stabiliseren. Laat uit als uitvoer al correct lijkt. <0>Meer informatie" diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 5cee81a016..fd90a26003 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} serwerów MCP", "messageTemplate": "Masz {{tools}} włączonych przez {{servers}}. Taka duża liczba może zamieszać model i prowadzić do błędów. Staraj się, aby była poniżej {{threshold}}.", "openMcpSettings": "Otwórz ustawienia MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 7c339f96a5..1ed4e59159 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -725,6 +725,15 @@ "label": "Limit znaków terminala", "description": "Zastępuje limit linii, aby zapobiec problemom z pamięcią, narzucając twardy limit rozmiaru wyjścia. W przypadku przekroczenia zachowuje początek i koniec i pokazuje symbol zastępczy Roo tam, gdzie treść jest pomijana. <0>Dowiedz się więcej" }, + "outputPreviewSize": { + "label": "Rozmiar podglądu wyjścia polecenia", + "description": "Kontroluje, ile wyjścia polecenia Roo widzi bezpośrednio. Pełne wyjście jest zawsze zapisywane i dostępne w razie potrzeby.", + "options": { + "small": "Mały (5KB)", + "medium": "Średni (10KB)", + "large": "Duży (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Limit czasu integracji powłoki terminala", "description": "Jak długo czekać na integrację powłoki VS Code przed wykonaniem poleceń. Zwiększ, jeśli twoja powłoka wolno się uruchamia lub widzisz błędy 'Integracja Powłoki Niedostępna'. <0>Dowiedz się więcej" @@ -737,10 +746,6 @@ "label": "Opóźnienie polecenia terminala", "description": "Dodaje krótką pauzę po każdym poleceniu, aby terminal VS Code mógł opróżnić całe wyjście (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Używaj tylko gdy widzisz brakujące wyjście końcowe; w przeciwnym razie zostaw na 0. <0>Dowiedz się więcej" }, - "compressProgressBar": { - "label": "Kompresuj wyjście paska postępu", - "description": "Zwija paski postępu/spinnery, aby zachować tylko stan końcowy (oszczędza tokeny). <0>Dowiedz się więcej" - }, "powershellCounter": { "label": "Włącz obejście licznika PowerShell", "description": "Włącz gdy brakuje lub jest zduplikowane wyjście PowerShell; dodaje mały licznik do każdego polecenia, aby ustabilizować wyjście. Pozostaw wyłączone, jeśli wyjście już wygląda poprawnie. <0>Dowiedz się więcej" diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 175b64f1ae..c6fdc35e82 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -504,5 +504,8 @@ "serversPart_other": "{{count}} servidores MCP", "messageTemplate": "Você tem {{tools}} habilitadas via {{servers}}. Um número tão alto pode confundir o modelo e levar a erros. Tente mantê-lo abaixo de {{threshold}}.", "openMcpSettings": "Abrir Configurações MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 04a786ab11..1d989db379 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -725,6 +725,15 @@ "label": "Limite de caracteres do terminal", "description": "Substitui o limite de linhas para evitar problemas de memória, impondo um limite rígido no tamanho da saída. Se excedido, mantém o início e o fim e mostra um placeholder para o Roo onde o conteúdo é pulado. <0>Saiba mais" }, + "outputPreviewSize": { + "label": "Tamanho da visualização da saída de comandos", + "description": "Controla quanto da saída de comandos Roo vê diretamente. A saída completa é sempre salva e acessível quando necessário.", + "options": { + "small": "Pequeno (5KB)", + "medium": "Médio (10KB)", + "large": "Grande (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Tempo limite de integração do shell do terminal", "description": "Quanto tempo esperar pela integração do shell do VS Code antes de executar comandos. Aumente se o seu shell demorar para iniciar ou se você vir erros de 'Integração do Shell Indisponível'. <0>Saiba mais" @@ -737,10 +746,6 @@ "label": "Atraso de comando do terminal", "description": "Adiciona uma pequena pausa após cada comando para que o terminal do VS Code possa liberar toda a saída (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Use apenas se você vir a saída final faltando; caso contrário, deixe em 0. <0>Saiba mais" }, - "compressProgressBar": { - "label": "Comprimir saída da barra de progresso", - "description": "Recolhe barras de progresso/spinners para que apenas o estado final seja mantido (economiza tokens). <0>Saiba mais" - }, "powershellCounter": { "label": "Ativar solução alternativa do contador do PowerShell", "description": "Ative isso quando a saída do PowerShell estiver faltando ou duplicada; ele adiciona um pequeno contador a cada comando para estabilizar a saída. Mantenha desativado se a saída já parecer correta. <0>Saiba mais" diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 56220c248a..dffbc64e8d 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -505,5 +505,8 @@ "serversPart_other": "{{count}} серверов MCP", "messageTemplate": "У тебя включено {{tools}} через {{servers}}. Такое большое количество может сбить модель с толку и привести к ошибкам. Постарайся держать это ниже {{threshold}}.", "openMcpSettings": "Открыть настройки MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index ebdbc01b93..5a736ef1ec 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -725,6 +725,15 @@ "label": "Лимит символов терминала", "description": "Переопределяет лимит строк для предотвращения проблем с памятью, устанавливая жёсткое ограничение на размер вывода. При превышении сохраняет начало и конец и показывает Roo заполнитель там, где контент пропущен. <0>Подробнее" }, + "outputPreviewSize": { + "label": "Размер предпросмотра вывода команд", + "description": "Контролирует, сколько вывода команды Roo видит напрямую. Полный вывод всегда сохраняется и доступен при необходимости.", + "options": { + "small": "Маленький (5KB)", + "medium": "Средний (10KB)", + "large": "Большой (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Таймаут интеграции shell терминала", "description": "Сколько ждать интеграции shell VS Code перед выполнением команд. Увеличьте, если ваш shell запускается медленно или вы видите ошибки 'Интеграция Shell Недоступна'. <0>Подробнее" @@ -737,10 +746,6 @@ "label": "Задержка команды терминала", "description": "Добавляет короткую паузу после каждой команды, чтобы терминал VS Code мог вывести весь output (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Используйте только если видите отсутствующий tail output; иначе оставьте 0. <0>Подробнее" }, - "compressProgressBar": { - "label": "Сжимать вывод прогресс-бара", - "description": "Сворачивает прогресс-бары/спиннеры, чтобы сохранялось только финальное состояние (экономит токены). <0>Подробнее" - }, "powershellCounter": { "label": "Включить обходчик счётчика PowerShell", "description": "Включите, когда вывод PowerShell отсутствует или дублируется; добавляет маленький счётчик к каждой команде для стабилизации вывода. Оставьте выключенным, если вывод уже выглядит корректно. <0>Подробнее" diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 4db29e7777..5d5d93893e 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -505,5 +505,8 @@ "serversPart_other": "{{count}} MCP sunucusu", "messageTemplate": "{{servers}} üzerinden {{tools}} etkinleştirilmiş durumda. Bu kadar fazlası modeli kafası karışabilir ve hatalara neden olabilir. {{threshold}} altında tutmaya çalış.", "openMcpSettings": "MCP Ayarlarını Aç" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 5a7daeec1d..9110f27d6e 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -725,6 +725,15 @@ "label": "Terminal karakter sınırı", "description": "Çıktı boyutuna katı bir üst sınır uygulayarak bellek sorunlarını önlemek için satır sınırını geçersiz kılar. Aşılırsa, başlangıcı ve sonu tutar ve içeriğin atlandığı yerde Roo'ya bir yer tutucu gösterir. <0>Daha fazla bilgi edinin" }, + "outputPreviewSize": { + "label": "Komut çıktısı önizleme boyutu", + "description": "Roo'nun doğrudan gördüğü komut çıktısı miktarını kontrol eder. Tam çıktı her zaman kaydedilir ve gerektiğinde erişilebilir.", + "options": { + "small": "Küçük (5KB)", + "medium": "Orta (10KB)", + "large": "Büyük (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Terminal shell entegrasyon timeout", "description": "Komut çalıştırmadan önce VS Code shell entegrasyonunu bekleme süresi. Shell yavaş başlıyorsa veya 'Shell Integration Unavailable' hatası görüyorsanız artırın. <0>Daha fazla bilgi edinin" @@ -737,10 +746,6 @@ "label": "Terminal komut delay", "description": "VS Code terminalin tüm outputu flush edebilmesi için her komuttan sonra kısa pause ekler (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Sadece tail output eksikse kullan; yoksa 0'da bırak. <0>Daha fazla bilgi edinin" }, - "compressProgressBar": { - "label": "İlerleme çubuğu çıktısını sıkıştır", - "description": "İlerleme çubukları/spinner'ları daraltır, sadece son durumu tutar (token tasarrufu). <0>Daha fazla bilgi edinin" - }, "powershellCounter": { "label": "PowerShell sayaç geçici çözümünü etkinleştir", "description": "PowerShell çıktısı eksik veya yineleniyorsa bunu açın; çıktıyı stabilize etmek için her komuta küçük bir sayaç ekler. Çıktı zaten doğru görünüyorsa bunu kapalı tutun. <0>Daha fazla bilgi edinin" diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 8e8a02e69d..76191a03cf 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -505,5 +505,8 @@ "serversPart_other": "{{count}} máy chủ MCP", "messageTemplate": "Bạn đã bật {{tools}} qua {{servers}}. Số lượng lớn như vậy có thể khiến mô hình bối rối và dẫn đến lỗi. Cố gắng giữ nó dưới {{threshold}}.", "openMcpSettings": "Mở cài đặt MCP" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 3e8e22d8f0..14c8904e09 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -725,6 +725,15 @@ "label": "Giới hạn ký tự terminal", "description": "Ghi đè giới hạn dòng để tránh vấn đề bộ nhớ bằng cách áp đặt giới hạn cứng cho kích thước đầu ra. Nếu vượt quá, giữ đầu và cuối, hiển thị placeholder cho Roo nơi nội dung bị bỏ qua. <0>Tìm hiểu thêm" }, + "outputPreviewSize": { + "label": "Kích thước xem trước đầu ra lệnh", + "description": "Kiểm soát lượng đầu ra lệnh mà Roo nhìn thấy trực tiếp. Đầu ra đầy đủ luôn được lưu và có thể truy cập khi cần thiết.", + "options": { + "small": "Nhỏ (5KB)", + "medium": "Trung bình (10KB)", + "large": "Lớn (20KB)" + } + }, "shellIntegrationTimeout": { "label": "Timeout tích hợp shell terminal", "description": "Thời gian đợi tích hợp shell VS Code trước khi chạy lệnh. Tăng nếu shell khởi động chậm hoặc thấy lỗi 'Shell Integration Unavailable'. <0>Tìm hiểu thêm" @@ -737,10 +746,6 @@ "label": "Delay lệnh terminal", "description": "Thêm khoảng dừng ngắn sau mỗi lệnh để VS Code terminal flush tất cả output (bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep). Chỉ dùng nếu thiếu tail output; nếu không để ở 0. <0>Tìm hiểu thêm" }, - "compressProgressBar": { - "label": "Nén đầu ra thanh tiến trình", - "description": "Thu gọn các thanh tiến trình/vòng quay để chỉ giữ lại trạng thái cuối cùng (tiết kiệm token). <0>Tìm hiểu thêm" - }, "powershellCounter": { "label": "Bật workaround bộ đếm PowerShell", "description": "Bật khi output PowerShell thiếu hoặc trùng lặp; thêm counter nhỏ vào mỗi lệnh để ổn định output. Tắt nếu output đã đúng. <0>Tìm hiểu thêm" diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index dadd731691..e63cc5dd08 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -505,5 +505,8 @@ "serversPart_other": "{{count}} 个 MCP 服务", "messageTemplate": "你通过 {{servers}} 启用了 {{tools}}。这么多数量会混淆模型并导致错误。建议将其保持在 {{threshold}} 以下。", "openMcpSettings": "打开 MCP 设置" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 85bc9bd431..1b58484ced 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -725,6 +725,15 @@ "label": "终端字符限制", "description": "通过强制限制输出大小来覆盖行限制以防止内存问题。如果超出,保留开头和结尾并向 Roo 显示内容被跳过的占位符。<0>了解更多" }, + "outputPreviewSize": { + "label": "命令输出预览大小", + "description": "控制 Roo 直接看到的命令输出量。完整输出始终会被保存,需要时可以访问。", + "options": { + "small": "小 (5KB)", + "medium": "中 (10KB)", + "large": "大 (20KB)" + } + }, "shellIntegrationTimeout": { "label": "终端 shell 集成超时", "description": "运行命令前等待 VS Code shell 集成的时间。如果 shell 启动缓慢或看到 'Shell Integration Unavailable' 错误,请提高此值。<0>了解更多" @@ -737,10 +746,6 @@ "label": "终端命令延迟", "description": "在每个命令后添加短暂暂停,以便 VS Code 终端刷新所有输出(bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep)。仅在看到缺少尾部输出时使用;否则保持为 0。<0>了解更多" }, - "compressProgressBar": { - "label": "压缩进度条输出", - "description": "折叠进度条/旋转器,仅保留最终状态(节省 token)。<0>了解更多" - }, "powershellCounter": { "label": "启用 PowerShell 计数器解决方案", "description": "当 PowerShell 输出丢失或重复时启用此选项;它会为每个命令附加一个小计数器以稳定输出。如果输出已正常,请保持关闭。<0>了解更多" diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index e0f9e44312..95a96503ba 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -495,5 +495,8 @@ "serversPart_other": "{{count}} 個 MCP 伺服器", "messageTemplate": "您已啟用 {{tools}}(透過 {{servers}})。這麼多的工具可能會混淆模型並導致錯誤。請嘗試保持在 {{threshold}} 以下。", "openMcpSettings": "開啟 MCP 設定" + }, + "readCommandOutput": { + "title": "Roo read command output" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 47f614932b..d18a5c8443 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -733,6 +733,15 @@ "label": "終端機字元限制", "description": "透過強制限制輸出大小來覆寫行限制以防止記憶體問題。如果超出,保留開頭和結尾並向 Roo 顯示內容被跳過的佔位符。<0>了解更多" }, + "outputPreviewSize": { + "label": "命令輸出預覽大小", + "description": "控制 Roo 直接看到的命令輸出量。完整輸出始終會被儲存,需要時可以存取。", + "options": { + "small": "小 (5KB)", + "medium": "中 (10KB)", + "large": "大 (20KB)" + } + }, "shellIntegrationTimeout": { "label": "終端機 shell 整合逾時", "description": "執行命令前等待 VS Code shell 整合的時間。如果 shell 啟動緩慢或看到 'Shell Integration Unavailable' 錯誤,請提高此值。<0>了解更多" @@ -745,10 +754,6 @@ "label": "終端機命令延遲", "description": "在每個命令後新增短暫暫停,以便 VS Code 終端機刷新所有輸出(bash/zsh: PROMPT_COMMAND sleep; PowerShell: start-sleep)。僅在看到缺少尾部輸出時使用;否則保持為 0。<0>了解更多" }, - "compressProgressBar": { - "label": "壓縮進度條輸出", - "description": "折疊進度條/旋轉器,僅保留最終狀態(節省 Token)。<0>了解更多" - }, "powershellCounter": { "label": "啟用 PowerShell 計數器解決方案", "description": "當 PowerShell 輸出遺失或重複時啟用此選項;它會為每個命令附加一個小計數器以穩定輸出。如果輸出已正常,請保持關閉。<0>了解更多" From f5004ac40a66439fffef59fef2edc2a04d097e43 Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:25:02 -0500 Subject: [PATCH 027/256] fix: prevent time-travel bug in parallel tool calling (#11046) --- src/core/task/Task.ts | 46 ++++++++ .../flushPendingToolResultsToHistory.spec.ts | 101 +++++++++++++++++- 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 09f61bf3a3..ff697d77a8 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -355,6 +355,20 @@ export class Task extends EventEmitter implements TaskLike { userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolResultBlockParam)[] = [] userMessageContentReady = false + /** + * Flag indicating whether the assistant message for the current streaming session + * has been saved to API conversation history. + * + * This is critical for parallel tool calling: tools should NOT execute until + * the assistant message is saved. Otherwise, if a tool like `new_task` triggers + * `flushPendingToolResultsToHistory()`, the user message with tool_results would + * appear BEFORE the assistant message with tool_uses, causing API errors. + * + * Reset to `false` at the start of each API request. + * Set to `true` after the assistant message is saved in `recursivelyMakeClineRequests`. + */ + assistantMessageSavedToHistory = false + /** * Push a tool_result block to userMessageContent, preventing duplicates. * Duplicate tool_use_ids cause API errors. @@ -1063,6 +1077,36 @@ export class Task extends EventEmitter implements TaskLike { return } + // CRITICAL: Wait for the assistant message to be saved to API history first. + // Without this, tool_result blocks would appear BEFORE tool_use blocks in the + // conversation history, causing API errors like: + // "unexpected `tool_use_id` found in `tool_result` blocks" + // + // This can happen when parallel tools are called (e.g., update_todo_list + new_task). + // Tools execute during streaming via presentAssistantMessage, BEFORE the assistant + // message is saved. When new_task triggers delegation, it calls this method to + // flush pending results - but the assistant message hasn't been saved yet. + // + // The assistantMessageSavedToHistory flag is: + // - Reset to false at the start of each API request + // - Set to true after the assistant message is saved in recursivelyMakeClineRequests + if (!this.assistantMessageSavedToHistory) { + await pWaitFor(() => this.assistantMessageSavedToHistory || this.abort, { + interval: 50, + timeout: 30_000, // 30 second timeout as safety net + }).catch(() => { + // If timeout or abort, log and proceed anyway to avoid hanging + console.warn( + `[Task#${this.taskId}] flushPendingToolResultsToHistory: timed out waiting for assistant message to be saved`, + ) + }) + } + + // If task was aborted while waiting, don't flush + if (this.abort) { + return + } + // Save the user message with tool_result blocks const userMessage: Anthropic.MessageParam = { role: "user", @@ -2707,6 +2751,7 @@ export class Task extends EventEmitter implements TaskLike { this.userMessageContentReady = false this.didRejectTool = false this.didAlreadyUseTool = false + this.assistantMessageSavedToHistory = false // Reset tool failure flag for each new assistant turn - this ensures that tool failures // only prevent attempt_completion within the same assistant message, not across turns // (e.g., if a tool fails, then user sends a message saying "just complete anyway") @@ -3488,6 +3533,7 @@ export class Task extends EventEmitter implements TaskLike { { role: "assistant", content: assistantContent }, reasoningMessage || undefined, ) + this.assistantMessageSavedToHistory = true TelemetryService.instance.captureConversationMessage(this.taskId, "assistant") } diff --git a/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts b/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts index 4f6f79970e..ca68347cbd 100644 --- a/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts +++ b/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts @@ -38,8 +38,12 @@ vi.mock("fs/promises", async (importOriginal) => { } }) +const { mockPWaitFor } = vi.hoisted(() => { + return { mockPWaitFor: vi.fn().mockImplementation(async () => Promise.resolve()) } +}) + vi.mock("p-wait-for", () => ({ - default: vi.fn().mockImplementation(async () => Promise.resolve()), + default: mockPWaitFor, })) vi.mock("vscode", () => { @@ -344,4 +348,99 @@ describe("flushPendingToolResultsToHistory", () => { expect((task.apiConversationHistory[0] as any).ts).toBeGreaterThanOrEqual(beforeTs) expect((task.apiConversationHistory[0] as any).ts).toBeLessThanOrEqual(afterTs) }) + + it("should skip waiting for assistantMessageSavedToHistory when flag is already true", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Set flag to true (assistant message already saved) + task.assistantMessageSavedToHistory = true + + // Set up pending tool result + task.userMessageContent = [ + { + type: "tool_result", + tool_use_id: "tool-skip-wait", + content: "Result when flag is true", + }, + ] + + // Clear mock call history + mockPWaitFor.mockClear() + + await task.flushPendingToolResultsToHistory() + + // Should not have called pWaitFor since flag was already true + expect(mockPWaitFor).not.toHaveBeenCalled() + + // Should still save the message + expect(task.apiConversationHistory.length).toBe(1) + expect((task.apiConversationHistory[0].content as any[])[0].tool_use_id).toBe("tool-skip-wait") + }) + + it("should wait for assistantMessageSavedToHistory when flag is false", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Flag is false by default - assistant message not yet saved + expect(task.assistantMessageSavedToHistory).toBe(false) + + // Set up pending tool result + task.userMessageContent = [ + { + type: "tool_result", + tool_use_id: "tool-wait", + content: "Result when flag is false", + }, + ] + + // Clear mock call history + mockPWaitFor.mockClear() + + await task.flushPendingToolResultsToHistory() + + // Should have called pWaitFor since flag was false + expect(mockPWaitFor).toHaveBeenCalled() + + // Should still save the message (mock resolves immediately) + expect(task.apiConversationHistory.length).toBe(1) + }) + + it("should not flush when task is aborted during wait", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + // Flag is false - will need to wait + task.assistantMessageSavedToHistory = false + + // Set up pending tool result + task.userMessageContent = [ + { + type: "tool_result", + tool_use_id: "tool-aborted", + content: "Should not be saved", + }, + ] + + // Set abort flag - this will cause the condition in pWaitFor to return true + // AND will cause early return after the wait + task.abort = true + + await task.flushPendingToolResultsToHistory() + + // Should not have saved anything since task was aborted + expect(task.apiConversationHistory.length).toBe(0) + }) }) From fe722dad23d6b39605c1779c0f3bb7871d6ee434 Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:05:53 -0500 Subject: [PATCH 028/256] feat: add AI SDK dependencies and message conversion utilities (#11047) --- package.json | 3 +- packages/types/package.json | 2 +- pnpm-lock.yaml | 209 ++++++--- src/api/transform/__tests__/ai-sdk.spec.ts | 492 +++++++++++++++++++++ src/api/transform/ai-sdk.ts | 282 ++++++++++++ src/package.json | 5 +- 6 files changed, 918 insertions(+), 75 deletions(-) create mode 100644 src/api/transform/__tests__/ai-sdk.spec.ts create mode 100644 src/api/transform/ai-sdk.ts diff --git a/package.json b/package.json index b93691d269..988072e981 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,8 @@ "bluebird": ">=3.7.2", "glob": ">=11.1.0", "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.5" + "@types/react-dom": "^18.3.5", + "zod": "3.25.76" } } } diff --git a/packages/types/package.json b/packages/types/package.json index 09fac8d672..d66d87ac72 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,7 +23,7 @@ "clean": "rimraf dist .turbo" }, "dependencies": { - "zod": "^3.25.61" + "zod": "3.25.76" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60bff02c0b..b8ca01240b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,7 @@ overrides: glob: '>=11.1.0' '@types/react': ^18.3.23 '@types/react-dom': ^18.3.5 + zod: 3.25.76 importers: @@ -268,7 +269,7 @@ importers: version: 0.518.0(react@18.3.1) next: specifier: ~15.2.8 - version: 15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -303,8 +304,8 @@ importers: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -377,7 +378,7 @@ importers: version: 0.518.0(react@18.3.1) next: specifier: ~15.2.8 - version: 15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -418,8 +419,8 @@ importers: specifier: ^6.1.86 version: 6.1.86 zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -444,7 +445,7 @@ importers: version: 10.4.21(postcss@8.5.4) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 4.2.3(next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) postcss: specifier: ^8.5.4 version: 8.5.4 @@ -455,8 +456,8 @@ importers: packages/build: dependencies: zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -489,7 +490,7 @@ importers: specifier: ^4.8.1 version: 4.8.1 zod: - specifier: ^3.25.76 + specifier: 3.25.76 version: 3.25.76 devDependencies: '@roo-code/config-eslint': @@ -564,7 +565,7 @@ importers: specifier: ^5.12.2 version: 5.12.2(ws@8.18.3)(zod@3.25.76) zod: - specifier: ^3.25.61 + specifier: 3.25.76 version: 3.25.76 devDependencies: '@roo-code/config-eslint': @@ -593,7 +594,7 @@ importers: version: 0.13.0 drizzle-orm: specifier: ^0.44.1 - version: 0.44.1(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7) + version: 0.44.1(@libsql/client@0.15.8)(@opentelemetry/api@1.9.0)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7) execa: specifier: ^9.6.0 version: 9.6.0 @@ -619,8 +620,8 @@ importers: specifier: ^5.5.5 version: 5.5.5 zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -681,8 +682,8 @@ importers: specifier: ^5.0.0 version: 5.1.1 zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -703,8 +704,8 @@ importers: packages/types: dependencies: zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -765,7 +766,7 @@ importers: version: 1.2.0 '@mistralai/mistralai': specifier: ^1.9.18 - version: 1.9.18(zod@3.25.61) + version: 1.9.18(zod@3.25.76) '@modelcontextprotocol/sdk': specifier: 1.12.0 version: 1.12.0 @@ -879,7 +880,7 @@ importers: version: 0.5.17 openai: specifier: ^5.12.2 - version: 5.12.2(ws@8.18.3)(zod@3.25.61) + version: 5.12.2(ws@8.18.3)(zod@3.25.76) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -986,9 +987,12 @@ importers: specifier: ^2.8.0 version: 2.8.0 zod: - specifier: 3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: + '@openrouter/ai-sdk-provider': + specifier: ^2.0.4 + version: 2.1.1(ai@6.0.57(zod@3.25.76))(zod@3.25.76) '@roo-code/build': specifier: workspace:^ version: link:../packages/build @@ -1013,9 +1017,6 @@ importers: '@types/glob': specifier: ^8.1.0 version: 8.1.0 - '@types/json-stream-stringify': - specifier: ^2.0.4 - version: 2.0.4 '@types/lodash.debounce': specifier: ^4.0.9 version: 4.0.9 @@ -1064,6 +1065,9 @@ importers: '@vscode/vsce': specifier: 3.3.2 version: 3.3.2 + ai: + specifier: ^6.0.0 + version: 6.0.57(zod@3.25.76) esbuild-wasm: specifier: ^0.25.0 version: 0.25.12 @@ -1099,7 +1103,7 @@ importers: version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) zod-to-ts: specifier: ^1.2.0 - version: 1.2.0(typescript@5.8.3)(zod@3.25.61) + version: 1.2.0(typescript@5.8.3)(zod@3.25.76) webview-ui: dependencies: @@ -1305,8 +1309,8 @@ importers: specifier: ^0.2.2 version: 0.2.2(@types/react@18.3.23)(react@18.3.1) zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -1374,6 +1378,22 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} + '@ai-sdk/gateway@3.0.25': + resolution: {integrity: sha512-j0AQeA7hOVqwImykQlganf/Euj3uEXf0h3G0O4qKTDpEwE+EZGIPnVimCWht5W91lAetPZSfavDyvfpuPDd2PQ==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.10': + resolution: {integrity: sha512-VeDAiCH+ZK8Xs4hb9Cw7pHlujWNL52RKe8TExOkrw6Ir1AmfajBZTb9XUdKOZO08RwQElIKA8+Ltm+Gqfo8djQ==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + + '@ai-sdk/provider@3.0.5': + resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} + engines: {node: '>=18'} + '@alcalzone/ansi-tokenize@0.2.3': resolution: {integrity: sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ==} engines: {node: '>=18'} @@ -2392,7 +2412,7 @@ packages: '@mistralai/mistralai@1.9.18': resolution: {integrity: sha512-D/vNAGEvWMsg95tzgLTg7pPnW9leOPyH+nh1Os05NwxVPbUykoYgMAwOEX7J46msahWdvZ4NQQuxUXIUV2P6dg==} peerDependencies: - zod: '>= 3' + zod: 3.25.76 '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -2591,6 +2611,17 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@openrouter/ai-sdk-provider@2.1.1': + resolution: {integrity: sha512-UypPbVnSExxmG/4Zg0usRiit3auvQVrjUXSyEhm0sZ9GQnW/d8p/bKgCk2neh1W5YyRSo7PNQvCrAEBHZnqQkQ==} + engines: {node: '>=18'} + peerDependencies: + ai: ^6.0.0 + zod: 3.25.76 + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@oxc-resolver/binding-darwin-arm64@11.2.0': resolution: {integrity: sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg==} cpu: [arm64] @@ -3845,6 +3876,9 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -4268,10 +4302,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json-stream-stringify@2.0.4': - resolution: {integrity: sha512-xSFsVnoQ8Y/7BiVF3/fEIwRx9RoGzssDKVwhy1g23wkA4GAmA3v8lsl6CxsmUD6vf4EiRd+J0ULLkMbAWRSsgQ==} - deprecated: This is a stub types definition. json-stream-stringify provides its own type definitions, so you do not need this installed. - '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} @@ -4469,6 +4499,10 @@ packages: resolution: {integrity: sha512-e4kQK9mP8ntpo3dACWirGod/hHv4qO5JMj9a/0a2AZto7b4persj5YP7t1Er372gTtYFTYxNhMx34jRvHooglw==} engines: {node: '>=16'} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@vitejs/plugin-react@4.4.1': resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4621,6 +4655,12 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + ai@6.0.57: + resolution: {integrity: sha512-5wYcMQmOaNU71wGv4XX1db3zvn4uLjLbTKIo6cQZPWOJElA0882XI7Eawx6TCd5jbjOvKMIP+KLWbpVomAFT2g==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -6200,6 +6240,10 @@ packages: resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} engines: {node: '>=18.0.0'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} @@ -7319,6 +7363,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -8317,7 +8364,7 @@ packages: hasBin: true peerDependencies: ws: ^8.18.0 - zod: ^3.23.8 + zod: 3.25.76 peerDependenciesMeta: ws: optional: true @@ -10688,25 +10735,19 @@ packages: zod-to-json-schema@3.24.5: resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} peerDependencies: - zod: ^3.24.1 + zod: 3.25.76 zod-to-ts@1.2.0: resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} peerDependencies: typescript: ^4.9.4 || ^5.0.2 - zod: ^3 + zod: 3.25.76 zod-validation-error@3.4.1: resolution: {integrity: sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==} engines: {node: '>=18.0.0'} peerDependencies: - zod: ^3.24.4 - - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - - zod@3.25.61: - resolution: {integrity: sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==} + zod: 3.25.76 zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -10736,6 +10777,24 @@ snapshots: '@adobe/css-tools@4.4.2': {} + '@ai-sdk/gateway@3.0.25(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.10(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.10(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@3.0.5': + dependencies: + json-schema: 0.4.0 + '@alcalzone/ansi-tokenize@0.2.3': dependencies: ansi-styles: 6.2.3 @@ -12250,10 +12309,10 @@ snapshots: dependencies: exenv-es6: 1.1.1 - '@mistralai/mistralai@1.9.18(zod@3.25.61)': + '@mistralai/mistralai@1.9.18(zod@3.25.76)': dependencies: - zod: 3.25.61 - zod-to-json-schema: 3.24.5(zod@3.25.61) + zod: 3.25.76 + zod-to-json-schema: 3.24.5(zod@3.25.76) '@mixmark-io/domino@2.2.0': {} @@ -12421,6 +12480,13 @@ snapshots: '@open-draft/until@2.1.0': {} + '@openrouter/ai-sdk-provider@2.1.1(ai@6.0.57(zod@3.25.76))(zod@3.25.76)': + dependencies: + ai: 6.0.57(zod@3.25.76) + zod: 3.25.76 + + '@opentelemetry/api@1.9.0': {} + '@oxc-resolver/binding-darwin-arm64@11.2.0': optional: true @@ -13822,6 +13888,8 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} '@swc/counter@0.1.3': {} @@ -14248,10 +14316,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/json-stream-stringify@2.0.4': - dependencies: - json-stream-stringify: 3.1.6 - '@types/katex@0.16.7': {} '@types/lodash.debounce@4.0.9': @@ -14483,6 +14547,8 @@ snapshots: satori: 0.12.2 yoga-wasm-web: 0.3.3 + '@vercel/oidc@3.1.0': {} + '@vitejs/plugin-react@4.4.1(vite@6.3.6(@types/node@20.17.57)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.1 @@ -14555,7 +14621,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -14705,6 +14771,14 @@ snapshots: dependencies: humanize-ms: 1.2.1 + ai@6.0.57(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.25(zod@3.25.76) + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.10(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -15232,7 +15306,7 @@ snapshots: dependencies: devtools-protocol: 0.0.1367902 mitt: 3.0.1 - zod: 3.23.8 + zod: 3.25.76 chromium-bidi@5.1.0(devtools-protocol@0.0.1452169): dependencies: @@ -15910,9 +15984,10 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.1(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7): + drizzle-orm@0.44.1(@libsql/client@0.15.8)(@opentelemetry/api@1.9.0)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7): optionalDependencies: '@libsql/client': 0.15.8 + '@opentelemetry/api': 1.9.0 better-sqlite3: 11.10.0 gel: 2.1.0 postgres: 3.4.7 @@ -16387,6 +16462,8 @@ snapshots: eventsource-parser@3.0.2: {} + eventsource-parser@3.0.6: {} + eventsource@3.0.7: dependencies: eventsource-parser: 3.0.2 @@ -17689,6 +17766,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stream-stringify@3.1.6: {} @@ -18740,20 +18819,20 @@ snapshots: netmask@2.0.2: {} - next-sitemap@4.2.3(next@15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + next-sitemap@4.2.3(next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.11 fast-glob: 3.3.3 minimist: 1.2.8 - next: 15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 15.2.8 '@swc/counter': 0.1.3 @@ -18773,6 +18852,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.2.5 '@next/swc-win32-arm64-msvc': 15.2.5 '@next/swc-win32-x64-msvc': 15.2.5 + '@opentelemetry/api': 1.9.0 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' @@ -18945,11 +19025,6 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@5.12.2(ws@8.18.3)(zod@3.25.61): - optionalDependencies: - ws: 8.18.3 - zod: 3.25.61 - openai@5.12.2(ws@8.18.3)(zod@3.25.76): optionalDependencies: ws: 8.18.3 @@ -21785,27 +21860,19 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-to-json-schema@3.24.5(zod@3.25.61): - dependencies: - zod: 3.25.61 - zod-to-json-schema@3.24.5(zod@3.25.76): dependencies: zod: 3.25.76 - zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.61): + zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.76): dependencies: typescript: 5.8.3 - zod: 3.25.61 + zod: 3.25.76 zod-validation-error@3.4.1(zod@3.25.76): dependencies: zod: 3.25.76 - zod@3.23.8: {} - - zod@3.25.61: {} - zod@3.25.76: {} zustand@5.0.9(@types/react@18.3.23)(react@19.2.3): diff --git a/src/api/transform/__tests__/ai-sdk.spec.ts b/src/api/transform/__tests__/ai-sdk.spec.ts new file mode 100644 index 0000000000..4a82ecac4e --- /dev/null +++ b/src/api/transform/__tests__/ai-sdk.spec.ts @@ -0,0 +1,492 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } from "../ai-sdk" + +vitest.mock("ai", () => ({ + tool: vitest.fn((t) => t), + jsonSchema: vitest.fn((s) => s), +})) + +describe("AI SDK conversion utilities", () => { + describe("convertToAiSdkMessages", () => { + it("converts simple string messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ role: "user", content: "Hello" }) + expect(result[1]).toEqual({ role: "assistant", content: "Hi there" }) + }) + + it("converts user messages with text content blocks", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [{ type: "text", text: "Hello world" }], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "user", + content: [{ type: "text", text: "Hello world" }], + }) + }) + + it("converts user messages with image content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "base64encodeddata", + }, + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + image: "data:image/png;base64,base64encodeddata", + mimeType: "image/png", + }, + ], + }) + }) + + it("converts user messages with URL image content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + source: { + type: "url", + url: "https://example.com/image.png", + }, + } as any, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + image: "https://example.com/image.png", + }, + ], + }) + }) + + it("converts tool results into separate tool role messages with resolved tool names", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call_123", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_123", + content: "Tool result content", + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call_123", + toolName: "read_file", + input: { path: "test.ts" }, + }, + ], + }) + // Tool results now go to role: "tool" messages per AI SDK v6 schema + expect(result[1]).toEqual({ + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call_123", + toolName: "read_file", + output: { type: "text", value: "Tool result content" }, + }, + ], + }) + }) + + it("uses unknown_tool for tool results without matching tool call", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_orphan", + content: "Orphan result", + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + // Tool results go to role: "tool" messages + expect(result[0]).toEqual({ + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call_orphan", + toolName: "unknown_tool", + output: { type: "text", value: "Orphan result" }, + }, + ], + }) + }) + + it("separates tool results and text content into different messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call_123", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_123", + content: "File contents here", + }, + { + type: "text", + text: "Please analyze this file", + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(3) + expect(result[0]).toEqual({ + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call_123", + toolName: "read_file", + input: { path: "test.ts" }, + }, + ], + }) + // Tool results go first in a "tool" message + expect(result[1]).toEqual({ + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call_123", + toolName: "read_file", + output: { type: "text", value: "File contents here" }, + }, + ], + }) + // Text content goes in a separate "user" message + expect(result[2]).toEqual({ + role: "user", + content: [{ type: "text", text: "Please analyze this file" }], + }) + }) + + it("converts assistant messages with tool use", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "text", text: "Let me read that file" }, + { + type: "tool_use", + id: "call_456", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "assistant", + content: [ + { type: "text", text: "Let me read that file" }, + { + type: "tool-call", + toolCallId: "call_456", + toolName: "read_file", + input: { path: "test.ts" }, + }, + ], + }) + }) + + it("handles empty assistant content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "assistant", + content: [{ type: "text", text: "" }], + }) + }) + }) + + describe("convertToolsForAiSdk", () => { + it("returns undefined for empty tools", () => { + expect(convertToolsForAiSdk(undefined)).toBeUndefined() + expect(convertToolsForAiSdk([])).toBeUndefined() + }) + + it("converts function tools to AI SDK format", () => { + const tools: OpenAI.Chat.ChatCompletionTool[] = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file from disk", + parameters: { + type: "object", + properties: { + path: { type: "string", description: "File path" }, + }, + required: ["path"], + }, + }, + }, + ] + + const result = convertToolsForAiSdk(tools) + + expect(result).toBeDefined() + expect(result!.read_file).toBeDefined() + expect(result!.read_file.description).toBe("Read a file from disk") + }) + + it("converts multiple tools", () => { + const tools: OpenAI.Chat.ChatCompletionTool[] = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "write_file", + description: "Write a file", + parameters: {}, + }, + }, + ] + + const result = convertToolsForAiSdk(tools) + + expect(result).toBeDefined() + expect(Object.keys(result!)).toHaveLength(2) + expect(result!.read_file).toBeDefined() + expect(result!.write_file).toBeDefined() + }) + }) + + describe("processAiSdkStreamPart", () => { + it("processes text-delta chunks", () => { + const part = { type: "text-delta" as const, id: "1", text: "Hello" } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "text", text: "Hello" }) + }) + + it("processes text chunks (fullStream format)", () => { + const part = { type: "text" as const, text: "Hello from fullStream" } + const chunks = [...processAiSdkStreamPart(part as any)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "text", text: "Hello from fullStream" }) + }) + + it("processes reasoning-delta chunks", () => { + const part = { type: "reasoning-delta" as const, id: "1", text: "thinking..." } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("processes reasoning chunks (fullStream format)", () => { + const part = { type: "reasoning" as const, text: "reasoning from fullStream" } + const chunks = [...processAiSdkStreamPart(part as any)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "reasoning", text: "reasoning from fullStream" }) + }) + + it("processes tool-input-start chunks", () => { + const part = { type: "tool-input-start" as const, id: "call_1", toolName: "read_file" } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "tool_call_start", id: "call_1", name: "read_file" }) + }) + + it("processes tool-input-delta chunks", () => { + const part = { type: "tool-input-delta" as const, id: "call_1", delta: '{"path":' } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "tool_call_delta", id: "call_1", delta: '{"path":' }) + }) + + it("processes tool-input-end chunks", () => { + const part = { type: "tool-input-end" as const, id: "call_1" } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "tool_call_end", id: "call_1" }) + }) + + it("processes complete tool-call chunks", () => { + const part = { + type: "tool-call" as const, + toolCallId: "call_1", + toolName: "read_file", + input: { path: "test.ts" }, + } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ + type: "tool_call", + id: "call_1", + name: "read_file", + arguments: '{"path":"test.ts"}', + }) + }) + + it("processes source chunks with URL", () => { + const part = { + type: "source" as const, + url: "https://example.com", + title: "Example Source", + } + const chunks = [...processAiSdkStreamPart(part as any)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ + type: "grounding", + sources: [ + { + title: "Example Source", + url: "https://example.com", + snippet: undefined, + }, + ], + }) + }) + + it("processes error chunks", () => { + const part = { type: "error" as const, error: new Error("Test error") } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ + type: "error", + error: "StreamError", + message: "Test error", + }) + }) + + it("ignores lifecycle events", () => { + const lifecycleEvents = [ + { type: "text-start" as const }, + { type: "text-end" as const }, + { type: "reasoning-start" as const }, + { type: "reasoning-end" as const }, + { type: "start-step" as const }, + { type: "finish-step" as const }, + { type: "start" as const }, + { type: "finish" as const }, + { type: "abort" as const }, + ] + + for (const event of lifecycleEvents) { + const chunks = [...processAiSdkStreamPart(event as any)] + expect(chunks).toHaveLength(0) + } + }) + }) +}) diff --git a/src/api/transform/ai-sdk.ts b/src/api/transform/ai-sdk.ts new file mode 100644 index 0000000000..535b932aba --- /dev/null +++ b/src/api/transform/ai-sdk.ts @@ -0,0 +1,282 @@ +/** + * AI SDK conversion utilities for transforming between Anthropic/OpenAI formats and Vercel AI SDK formats. + * These utilities are designed to be reused across different AI SDK providers. + */ + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { tool as createTool, jsonSchema, type ModelMessage, type TextStreamPart } from "ai" +import type { ApiStreamChunk } from "./stream" + +/** + * Convert Anthropic messages to AI SDK ModelMessage format. + * Handles text, images, tool uses, and tool results. + * + * @param messages - Array of Anthropic message parameters + * @returns Array of AI SDK ModelMessage objects + */ +export function convertToAiSdkMessages(messages: Anthropic.Messages.MessageParam[]): ModelMessage[] { + const modelMessages: ModelMessage[] = [] + + // First pass: build a map of tool call IDs to tool names from assistant messages + const toolCallIdToName = new Map() + for (const message of messages) { + if (message.role === "assistant" && typeof message.content !== "string") { + for (const part of message.content) { + if (part.type === "tool_use") { + toolCallIdToName.set(part.id, part.name) + } + } + } + } + + for (const message of messages) { + if (typeof message.content === "string") { + modelMessages.push({ + role: message.role, + content: message.content, + }) + } else { + if (message.role === "user") { + const parts: Array< + { type: "text"; text: string } | { type: "image"; image: string; mimeType?: string } + > = [] + const toolResults: Array<{ + type: "tool-result" + toolCallId: string + toolName: string + output: { type: "text"; value: string } + }> = [] + + for (const part of message.content) { + if (part.type === "text") { + parts.push({ type: "text", text: part.text }) + } else if (part.type === "image") { + // Handle both base64 and URL source types + const source = part.source as { type: string; media_type?: string; data?: string; url?: string } + if (source.type === "base64" && source.media_type && source.data) { + parts.push({ + type: "image", + image: `data:${source.media_type};base64,${source.data}`, + mimeType: source.media_type, + }) + } else if (source.type === "url" && source.url) { + parts.push({ + type: "image", + image: source.url, + }) + } + } else if (part.type === "tool_result") { + // Convert tool results to string content + let content: string + if (typeof part.content === "string") { + content = part.content + } else { + content = + part.content + ?.map((c) => { + if (c.type === "text") return c.text + if (c.type === "image") return "(image)" + return "" + }) + .join("\n") ?? "" + } + // Look up the tool name from the tool call ID + const toolName = toolCallIdToName.get(part.tool_use_id) ?? "unknown_tool" + toolResults.push({ + type: "tool-result", + toolCallId: part.tool_use_id, + toolName, + output: { type: "text", value: content || "(empty)" }, + }) + } + } + + // AI SDK requires tool results in separate "tool" role messages + // UserContent only supports: string | Array + // ToolContent (for role: "tool") supports: Array + if (toolResults.length > 0) { + modelMessages.push({ + role: "tool", + content: toolResults, + } as ModelMessage) + } + + // Add user message with only text/image content (no tool results) + if (parts.length > 0) { + modelMessages.push({ + role: "user", + content: parts, + } as ModelMessage) + } + } else if (message.role === "assistant") { + const textParts: string[] = [] + const toolCalls: Array<{ + type: "tool-call" + toolCallId: string + toolName: string + input: unknown + }> = [] + + for (const part of message.content) { + if (part.type === "text") { + textParts.push(part.text) + } else if (part.type === "tool_use") { + toolCalls.push({ + type: "tool-call", + toolCallId: part.id, + toolName: part.name, + input: part.input, + }) + } + } + + const content: Array< + | { type: "text"; text: string } + | { type: "tool-call"; toolCallId: string; toolName: string; input: unknown } + > = [] + + if (textParts.length > 0) { + content.push({ type: "text", text: textParts.join("\n") }) + } + content.push(...toolCalls) + + modelMessages.push({ + role: "assistant", + content: content.length > 0 ? content : [{ type: "text", text: "" }], + } as ModelMessage) + } + } + } + + return modelMessages +} + +/** + * Convert OpenAI-style function tool definitions to AI SDK tool format. + * + * @param tools - Array of OpenAI tool definitions + * @returns Record of AI SDK tools keyed by tool name, or undefined if no tools + */ +export function convertToolsForAiSdk( + tools: OpenAI.Chat.ChatCompletionTool[] | undefined, +): Record> | undefined { + if (!tools || tools.length === 0) { + return undefined + } + + const toolSet: Record> = {} + + for (const t of tools) { + if (t.type === "function") { + toolSet[t.function.name] = createTool({ + description: t.function.description, + inputSchema: jsonSchema(t.function.parameters as any), + }) + } + } + + return toolSet +} + +/** + * Extended stream part type that includes additional fullStream event types + * that are emitted at runtime but not included in the AI SDK TextStreamPart type definitions. + */ +type ExtendedStreamPart = TextStreamPart | { type: "text"; text: string } | { type: "reasoning"; text: string } + +/** + * Process a single AI SDK stream part and yield the appropriate ApiStreamChunk(s). + * This generator handles all TextStreamPart types and converts them to the + * ApiStreamChunk format used by the application. + * + * @param part - The AI SDK TextStreamPart to process (including fullStream event types) + * @yields ApiStreamChunk objects corresponding to the stream part + */ +export function* processAiSdkStreamPart(part: ExtendedStreamPart): Generator { + switch (part.type) { + case "text": + case "text-delta": + yield { type: "text", text: (part as { text: string }).text } + break + + case "reasoning": + case "reasoning-delta": + yield { type: "reasoning", text: (part as { text: string }).text } + break + + case "tool-input-start": + yield { + type: "tool_call_start", + id: part.id, + name: part.toolName, + } + break + + case "tool-input-delta": + yield { + type: "tool_call_delta", + id: part.id, + delta: part.delta, + } + break + + case "tool-input-end": + yield { + type: "tool_call_end", + id: part.id, + } + break + + case "tool-call": + // Complete tool call - emit for compatibility + yield { + type: "tool_call", + id: part.toolCallId, + name: part.toolName, + arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input), + } + break + + case "source": + // Handle both URL and document source types + if ("url" in part) { + yield { + type: "grounding", + sources: [ + { + title: part.title || "Source", + url: part.url, + snippet: undefined, + }, + ], + } + } + break + + case "error": + yield { + type: "error", + error: "StreamError", + message: part.error instanceof Error ? part.error.message : String(part.error), + } + break + + // Ignore lifecycle events that don't need to yield chunks + case "text-start": + case "text-end": + case "reasoning-start": + case "reasoning-end": + case "start-step": + case "finish-step": + case "start": + case "finish": + case "abort": + case "file": + case "tool-result": + case "tool-error": + case "raw": + // These events don't need to be yielded + break + } +} diff --git a/src/package.json b/src/package.json index 97d0385898..bf4a009a94 100644 --- a/src/package.json +++ b/src/package.json @@ -529,9 +529,10 @@ "web-tree-sitter": "^0.25.6", "workerpool": "^9.2.0", "yaml": "^2.8.0", - "zod": "3.25.61" + "zod": "3.25.76" }, "devDependencies": { + "@openrouter/ai-sdk-provider": "^2.0.4", "@roo-code/build": "workspace:^", "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", @@ -540,7 +541,6 @@ "@types/diff": "^5.2.1", "@types/diff-match-patch": "^1.0.36", "@types/glob": "^8.1.0", - "@types/json-stream-stringify": "^2.0.4", "@types/lodash.debounce": "^4.0.9", "@types/mocha": "^10.0.10", "@types/node": "20.x", @@ -557,6 +557,7 @@ "@types/vscode": "^1.84.0", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "3.3.2", + "ai": "^6.0.0", "esbuild-wasm": "^0.25.0", "execa": "^9.5.2", "glob": "^11.1.0", From 8640fd14720da0a7da90ff565b6d495f766f43fb Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:29:34 -0500 Subject: [PATCH 029/256] fix: calculate header percentage based on available input space (#11054) Co-authored-by: Roo Code --- webview-ui/src/components/chat/TaskHeader.tsx | 10 ++- .../chat/__tests__/TaskHeader.spec.tsx | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 8948302ce1..d5424b7422 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -282,9 +282,13 @@ const TaskHeader = ({ sideOffset={8}> {(() => { - const percentage = Math.round( - (((contextTokens || 0) + reservedForOutput) / contextWindow) * 100, - ) + // Calculate percentage of available input space used + // Available input space = context window - reserved for output + const availableInputSpace = contextWindow - reservedForOutput + const percentage = + availableInputSpace > 0 + ? Math.round(((contextTokens || 0) / availableInputSpace) * 100) + : 0 return ( <> diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 07aa5480af..c4ebe06973 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -91,6 +91,26 @@ vi.mock("@roo/array", () => ({ }, })) +// Create a variable to hold the mock model info for useSelectedModel +let mockModelInfo: { contextWindow: number; maxTokens: number } | undefined = undefined + +// Mock useSelectedModel hook +vi.mock("@/components/ui/hooks/useSelectedModel", () => ({ + useSelectedModel: () => ({ + provider: "anthropic", + id: "test-model", + info: mockModelInfo, + isLoading: false, + isError: false, + }), +})) + +// Mock getModelMaxOutputTokens from @roo/api +let mockMaxOutputTokens = 0 +vi.mock("@roo/api", () => ({ + getModelMaxOutputTokens: () => mockMaxOutputTokens, +})) + describe("TaskHeader", () => { const defaultProps: TaskHeaderProps = { task: { type: "say", ts: Date.now(), text: "Test task", images: [] }, @@ -402,4 +422,52 @@ describe("TaskHeader", () => { expect(backButton?.querySelector("svg.lucide-arrow-left")).toBeInTheDocument() }) }) + + describe("Context window percentage calculation", () => { + // The percentage should be calculated as: + // contextTokens / (contextWindow - reservedForOutput) * 100 + // This represents the percentage of AVAILABLE input space used, + // not the percentage of the total context window. + + beforeEach(() => { + // Set up mock model with known contextWindow + mockModelInfo = { contextWindow: 1000, maxTokens: 200 } + // Set up mock for getModelMaxOutputTokens to return reservedForOutput + mockMaxOutputTokens = 200 + }) + + afterEach(() => { + // Reset mocks + mockModelInfo = undefined + mockMaxOutputTokens = 0 + }) + + it("should calculate percentage based on available input space, not total context window", () => { + // With the formula: contextTokens / (contextWindow - reservedForOutput) * 100 + // If contextTokens = 200, contextWindow = 1000, reservedForOutput = 200 + // Then available input space = 1000 - 200 = 800 + // Percentage = 200 / 800 * 100 = 25% + // + // Old (incorrect) formula would have been: (200 + 200) / 1000 * 100 = 40% + + renderTaskHeader({ contextTokens: 200 }) + + // The percentage should be rendered in the collapsed header state + // Verify that 25% is displayed (correct formula) and NOT 40% (old incorrect formula) + expect(screen.getByText("25%")).toBeInTheDocument() + expect(screen.queryByText("40%")).not.toBeInTheDocument() + }) + + it("should handle edge case when available input space is zero", () => { + // When contextWindow equals reservedForOutput, available space is 0 + // The percentage should be 0 to avoid division by zero + mockModelInfo = { contextWindow: 200, maxTokens: 200 } + mockMaxOutputTokens = 200 + + renderTaskHeader({ contextTokens: 100 }) + + // Should show 0% when available input space is 0 + expect(screen.getByText("0%")).toBeInTheDocument() + }) + }) }) From d7fa963b136e2382d6bb961223f24edfa635f866 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 28 Jan 2026 13:30:01 -0700 Subject: [PATCH 030/256] docs: clarify read_command_output search param should be omitted when not filtering (#11056) --- src/core/prompts/tools/native-tools/read_command_output.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/prompts/tools/native-tools/read_command_output.ts b/src/core/prompts/tools/native-tools/read_command_output.ts index 007915b005..44c069be1e 100644 --- a/src/core/prompts/tools/native-tools/read_command_output.ts +++ b/src/core/prompts/tools/native-tools/read_command_output.ts @@ -20,7 +20,7 @@ The tool supports two modes: Parameters: - artifact_id: (required) The artifact filename from the truncated output message (e.g., "cmd-1706119234567.txt") -- search: (optional) Pattern to filter lines. Supports regex or literal strings. Case-insensitive. +- search: (optional) Pattern to filter lines. Supports regex or literal strings. Case-insensitive. **Omit this parameter entirely if you don't need to filter - do not pass null or empty string.** - offset: (optional) Byte offset to start reading from. Default: 0. Use for pagination. - limit: (optional) Maximum bytes to return. Default: 40KB. @@ -38,7 +38,7 @@ Example: Finding specific test failures const ARTIFACT_ID_DESCRIPTION = `The artifact filename from the truncated command output (e.g., "cmd-1706119234567.txt")` -const SEARCH_DESCRIPTION = `Optional regex or literal pattern to filter lines (case-insensitive, like grep)` +const SEARCH_DESCRIPTION = `Optional regex or literal pattern to filter lines (case-insensitive, like grep). Omit this parameter if not searching - do not pass null or empty string.` const OFFSET_DESCRIPTION = `Byte offset to start reading from (default: 0, for pagination)` From c983e26280f67981f0a6b6bcc030f8735a26ba71 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:06:08 -0800 Subject: [PATCH 031/256] feat(marketing): add Linear integration page (#11028) Co-authored-by: Roo Code Co-authored-by: Michael Preuss --- apps/web-roo-code/src/app/linear/page.tsx | 413 ++++++++++++++++ .../src/components/chromes/nav-bar.tsx | 20 + .../components/linear/linear-issue-demo.tsx | 442 ++++++++++++++++++ 3 files changed, 875 insertions(+) create mode 100644 apps/web-roo-code/src/app/linear/page.tsx create mode 100644 apps/web-roo-code/src/components/linear/linear-issue-demo.tsx diff --git a/apps/web-roo-code/src/app/linear/page.tsx b/apps/web-roo-code/src/app/linear/page.tsx new file mode 100644 index 0000000000..40334e2698 --- /dev/null +++ b/apps/web-roo-code/src/app/linear/page.tsx @@ -0,0 +1,413 @@ +import { + ArrowRight, + CheckCircle, + CreditCard, + Eye, + GitBranch, + GitPullRequest, + Link2, + MessageSquare, + Settings, + Shield, +} from "lucide-react" +import type { LucideIcon } from "lucide-react" +import type { Metadata } from "next" + +import { AnimatedBackground } from "@/components/homepage" +import { LinearIssueDemo } from "@/components/linear/linear-issue-demo" +import { Button } from "@/components/ui" +import { EXTERNAL_LINKS } from "@/lib/constants" +import { SEO } from "@/lib/seo" +import { ogImageUrl } from "@/lib/og" + +const TITLE = "Roo Code for Linear" +const DESCRIPTION = "Assign development work to @Roo Code directly from Linear. Get PRs back without switching tools." +const OG_DESCRIPTION = "Turn Linear Issues into Pull Requests" +const PATH = "/linear" + +// Featured Workflow section is temporarily commented out until video is ready +// const LINEAR_DEMO_YOUTUBE_ID = "" + +export const metadata: Metadata = { + title: TITLE, + description: DESCRIPTION, + alternates: { + canonical: `${SEO.url}${PATH}`, + }, + openGraph: { + title: TITLE, + description: DESCRIPTION, + url: `${SEO.url}${PATH}`, + siteName: SEO.name, + images: [ + { + url: ogImageUrl(TITLE, OG_DESCRIPTION), + width: 1200, + height: 630, + alt: TITLE, + }, + ], + locale: SEO.locale, + type: "website", + }, + twitter: { + card: SEO.twitterCard, + title: TITLE, + description: DESCRIPTION, + images: [ogImageUrl(TITLE, OG_DESCRIPTION)], + }, + keywords: [ + ...SEO.keywords, + "linear integration", + "issue to PR", + "AI in Linear", + "engineering workflow automation", + "Roo Code Cloud", + ], +} + +// Invalidate cache when a request comes in, at most once every hour. +export const revalidate = 3600 + +type ValueProp = { + icon: LucideIcon + title: string + description: string +} + +const VALUE_PROPS: ValueProp[] = [ + { + icon: GitBranch, + title: "Work where you already work.", + description: + "Assign development work to @Roo Code directly from Linear. No new tools to learn, no context switching required.", + }, + { + icon: Eye, + title: "Progress is visible.", + description: + "Watch progress unfold in real-time. Roo Code posts updates as comments, so your whole team stays in the loop.", + }, + { + icon: MessageSquare, + title: "Mention for refinement.", + description: + 'Need changes? Just comment "@Roo Code also add dark mode support" and the agent picks up where it left off.', + }, + { + icon: Link2, + title: "Full traceability.", + description: + "Every PR links back to the originating issue. Every issue shows its linked PR. Your audit trail stays clean.", + }, + { + icon: Settings, + title: "Organization-level setup.", + description: + "Connect once, use everywhere. Your team members can assign issues to @Roo Code without individual configuration.", + }, + { + icon: Shield, + title: "Safe by design.", + description: + "Agents never touch main/master directly. They produce branches and PRs. You review and approve before merge.", + }, +] + +// type WorkflowStep = { +// step: number +// title: string +// description: string +// } + +// const WORKFLOW_STEPS: WorkflowStep[] = [ +// { +// step: 1, +// title: "Create an issue", +// description: "Write your issue with acceptance criteria. Be as detailed as you like.", +// }, +// { +// step: 2, +// title: "Call @Roo Code", +// description: "Mention @Roo Code in a comment to start. The agent begins working immediately.", +// }, +// { +// step: 3, +// title: "Watch progress", +// description: "Roo Code posts status updates as comments. Refine with @-mentions if needed.", +// }, +// { +// step: 4, +// title: "Review the PR", +// description: "When ready, the PR link appears in the issue. Review, iterate, and ship.", +// }, +// ] + +type OnboardingStep = { + icon: LucideIcon + title: string + description: string + link?: { + href: string + text: string + } +} + +const ONBOARDING_STEPS: OnboardingStep[] = [ + { + icon: CreditCard, + title: "1. Team Plan", + description: "Linear integration requires a Team plan.", + link: { + href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL, + text: "Start a free trial", + }, + }, + { + icon: GitPullRequest, + title: "2. Connect GitHub", + description: "Link your repositories so Roo Code can open PRs on your behalf.", + }, + { + icon: Settings, + title: "3. Connect Linear", + description: "Authorize via OAuth. No API keys to manage or rotate.", + }, + { + icon: CheckCircle, + title: "4. Link & Start", + description: "Map your Linear project to a repo, then assign or mention @Roo Code.", + }, +] + +function LinearIcon({ className }: { className?: string }) { + return ( + + + + ) +} + +export default function LinearPage(): JSX.Element { + return ( + <> + {/* Hero Section */} + + + {/* Value Props Section */} +
+
+
+
+
+
+

+ Why your team will love using Roo Code in Linear +

+

+ AI agents that understand context, keep your team in the loop, and deliver PRs you can + review. +

+
+
+ {VALUE_PROPS.map((prop, index) => { + const Icon = prop.icon + return ( +
+
+ +
+

{prop.title}

+

{prop.description}

+
+ ) + })} +
+
+
+ + {/* Featured Workflow Section - temporarily commented out until video is ready +
+
+
+
+
+ +
+
+ + Featured Workflow +
+

Issue to Shipped Feature

+

+ Stay in Linear from assignment to review. Roo Code keeps the issue updated and links the PR + when it's ready. +

+
+ +
+
+ {/* YouTube Video Embed or Placeholder */} + {/*
+ {LINEAR_DEMO_YOUTUBE_ID ? ( +
+ + +