Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## [v4.205.0] - 2025-12-26

### Added

- New chat renderer utility for improved message rendering
- Retry button functionality for API streaming failures
- Enhanced checkpoint handling system

### Changed

- Migrated KiloTaskHeader and TaskItem components to use ReadOnlyChatText
- Refactored ChatTextArea component with code cleanup
- Improved ChatRow component with better checkpoint integration

### Fixed

- Fixed duplicate code removal in ChatTextArea
- Resolved race condition with isUserInput flag
- Fixed previous commands still showing run/reject buttons
- Improved UI consistency and functionality

---

## [v4.204.1] - 2025-12-20

### Added
Expand Down
83 changes: 65 additions & 18 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3198,7 +3198,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
errorMsg = "Unknown error"
}

const baseDelay = requestDelaySeconds || 5
await this.ask("api_req_failed", errorMsg)

// Wait for the delay before retrying
const baseDelay = requestDelaySeconds || 0
let exponentialDelay = Math.min(
Math.ceil(baseDelay * Math.pow(2, retryAttempt)),
MAX_EXPONENTIAL_BACKOFF_SECONDS,
Expand All @@ -3212,21 +3215,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (geminiRetryDetails) {
const match = geminiRetryDetails?.retryDelay?.match(/^(\d+)s$/)
if (match) {
exponentialDelay = Number(match[1]) + 1
exponentialDelay = parseInt(match[1], 10)
}
}
}

// Wait for the greater of the exponential delay or the rate limit delay
const finalDelay = Math.max(exponentialDelay, rateLimitDelay)

// Show countdown timer with exponential backoff
for (let i = finalDelay; i > 0; i--) {
for (let i = exponentialDelay; i > 0; i--) {
await this.say(
"api_req_retry_delayed",
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`,
undefined,
true,
)
await delay(1000)
}
Expand All @@ -3235,7 +3233,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
"api_req_retry_delayed",
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying now...`,
undefined,
false,
)

// Delegate generator output from the recursive call with
Expand Down Expand Up @@ -3263,15 +3260,65 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}

// No error, so we can continue to yield all remaining chunks.
// (Needs to be placed outside of try/catch since it we want caller to
// handle errors not with api_req_failed as that is reserved for first
// chunk failures only.)
// This delegates to another generator or iterable object. In this case,
// it's saying "yield all remaining values from this iterator". This
// effectively passes along all subsequent chunks from the original
// stream.
yield* iterator
// No error on first chunk, so we can continue to yield all remaining chunks.
// Wrap in try/catch to handle mid-stream errors and allow retry.
try {
yield* iterator
} catch (error) {
// Reset streaming state since we encountered an error
this.isStreaming = false

// kilocode_change start
if (apiConfiguration?.apiProvider === "kilocode" && isAnyRecognizedKiloCodeError(error)) {
const { response } = await (isPaymentRequiredError(error)
? this.ask(
"payment_required_prompt",
JSON.stringify({
title: error.error?.title ?? t("kilocode:lowCreditWarning.title"),
message: error.error?.message ?? t("kilocode:lowCreditWarning.message"),
balance: error.error?.balance ?? "0.00",
buyCreditsUrl: error.error?.buyCreditsUrl ?? getAppUrl("/profile"),
}),
)
: this.ask(
"invalid_model",
JSON.stringify({
modelId: apiConfiguration.kilocodeModel,
error: {
status: error.status,
message: error.message,
},
}),
))

if (response === "retry_clicked") {
yield* this.attemptApiRequest(retryAttempt + 1)
} else {
// Handle other responses or cancellations if necessary
throw error // Rethrow to signal failure upwards
}
return
}
// kilocode_change end

// For mid-stream failures, show the retry dialog to allow user to retry
const { response } = await this.ask(
"api_req_failed",
error.message ?? JSON.stringify(serializeError(error), null, 2),
)

if (response !== "yesButtonClicked") {
// This will never happen since if noButtonClicked, we will
// clear current task, aborting this instance.
throw new Error("API request failed")
}

await this.say("api_req_retried")

// Delegate generator output from the recursive call.
yield* this.attemptApiRequest()
return
}
}

// Checkpoints
Expand Down
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "matterai",
"version": "4.204.1",
"version": "4.205.0",
"icon": "assets/icons/matterai-ic.png",
"galleryBanner": {
"color": "#FFFFFF",
Expand Down
43 changes: 30 additions & 13 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Trans, useTranslation } from "react-i18next"
import { useSize } from "react-use"
import { Undo2 } from "lucide-react"

import type { ClineMessage, FollowUpData, SuggestionItem } from "@roo-code/types"
import { Mode } from "@roo/modes"
Expand Down Expand Up @@ -34,9 +35,9 @@ import { CommandExecution } from "./CommandExecution"
import { CommandExecutionError } from "./CommandExecutionError"
import { FollowUpSuggest } from "./FollowUpSuggest"
import { Markdown } from "./Markdown"
import { Mention } from "./Mention"
import { ProgressIndicator } from "./ProgressIndicator"
import ReportBugPreview from "./ReportBugPreview"
import { ReadOnlyChatText } from "./ReadOnlyChatText"

import { cn } from "@/lib/utils"
import { appendImages } from "@src/utils/imageUtils"
Expand Down Expand Up @@ -1323,7 +1324,9 @@ export const ChatRowContent = ({
<div
className={cn(
"mb-2",
"border rounded-lg whitespace-pre-wrap",
"mr-2",
"rounded-lg whitespace-pre-wrap",
"bg-vscode-editor-background",
isEditing ? "overflow-visible" : "overflow-hidden", // kilocode_change
isEditing ? "text-vscode-editor-foreground" : "cursor-text p-1",
)}>
Expand All @@ -1347,18 +1350,32 @@ export const ChatRowContent = ({
/>
</div>
) : (
<div className="flex justify-between">
<div
className="flex-grow px-2 py-1 wrap-anywhere rounded-lg transition-colors"
onClick={(e) => {
e.stopPropagation()
if (!isStreaming) {
handleEditClick()
}
}}
title={t("chat:queuedMessages.clickToEdit")}>
<Mention text={message.text} withShadow />
<div className="flex justify-between items-end">
<div className="flex-grow">
<ReadOnlyChatText
value={message.text || ""}
className="px-2 py-1 wrap-anywhere rounded-lg transition-colors hover:bg-vscode-editor-hover-highlight"
onClick={() => {
if (!isStreaming) {
handleEditClick()
}
}}
title={t("chat:queuedMessages.clickToEdit")}
/>
</div>

<StandardTooltip content={t("chat:checkpoint.menu.restore")}>
<div
className="cursor-pointer shrink-0 mb-1.5 opacity-20 hover:opacity-100 transition-opacity"
style={{ visibility: isStreaming ? "hidden" : "visible" }}
onClick={(e) => {
e.stopPropagation()
handleEditClick()
}}
title={t("chat:checkpoint.restore")}>
<Undo2 className="w-3.5 h-3.5" />
</div>
</StandardTooltip>
<div className="flex gap-2 pr-1">
<div
className="cursor-pointer shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
Expand Down
Loading
Loading