Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion evals/packages/types/src/roo-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,6 @@ export const isGlobalStateKey = (key: string): key is Keys<GlobalState> =>
export const clineAsks = [
"followup",
"command",
"command_output",
"completion_result",
"tool",
"api_req_failed",
Expand Down
17 changes: 12 additions & 5 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class Cline extends EventEmitter<ClineEvents> {
private askResponse?: ClineAskResponse
private askResponseText?: string
private askResponseImages?: string[]
private lastMessageTs?: number
public lastMessageTs?: number

// Not private since it needs to be accessible by tools.
consecutiveMistakeCount: number = 0
Expand Down Expand Up @@ -440,7 +440,6 @@ export class Cline extends EventEmitter<ClineEvents> {
*/
askTs = lastMessage.ts
this.lastMessageTs = askTs
// lastMessage.ts = askTs
lastMessage.text = text
lastMessage.partial = false
lastMessage.progressStatus = progressStatus
Expand All @@ -466,12 +465,20 @@ export class Cline extends EventEmitter<ClineEvents> {
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
}

console.log(`[ask / ${type} / ${askTs}] waiting for ask response`)

await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })

const isTsMismatch = this.lastMessageTs !== askTs

console.log(
`[ask / ${type} / ${askTs}] pWaitFor returned, askResponse = ${this.askResponse}, isTsMismatch = ${isTsMismatch}`,
)

if (this.lastMessageTs !== askTs) {
// Could happen if we send multiple asks in a row i.e. with
// command_output. It's important that when we know an ask could
// fail, it is handled gracefully.
// Could happen if we send multiple asks in a row.
// It's important that when we know an ask could fail it is handled
// gracefully.
throw new Error("Current ask promise was ignored")
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/tools/attemptCompletionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export async function attemptCompletionTool(
}

// tell the provider to remove the current subtask and resume the previous task in the stack
await cline.providerRef.deref()?.finishSubTask(lastMessage?.text ?? "")
await cline.providerRef.deref()?.finishSubTask(result)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to make a lot more sense - hopefully not missing any obvious reason to use the lastMessage text

Copy link
Member

@daniel-lxs daniel-lxs May 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only the result parameter from the attempt_completion tool right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly. Seems like that's what we want to return to the parent task.

return
}

Expand Down
30 changes: 9 additions & 21 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, To
import { formatResponse } from "../prompts/responses"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
import { ExitCodeDetails, RooTerminalCallbacks } from "../../integrations/terminal/types"
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
import { Terminal } from "../../integrations/terminal/Terminal"

Expand Down Expand Up @@ -48,14 +48,15 @@ export async function executeCommandTool(

cline.consecutiveMistakeCount = 0

const executionId = Date.now().toString()
command = unescapeHtmlEntities(command) // Unescape HTML entities.
const didApprove = await askApproval("command", command, { id: executionId })
const didApprove = await askApproval("command", command)

if (!didApprove) {
return
}

const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString()

const clineProvider = await cline.providerRef.deref()
const clineProviderState = await clineProvider?.getState()
const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {}
Expand Down Expand Up @@ -140,7 +141,6 @@ export async function executeCommand(
}

let message: { text?: string; images?: string[] } | undefined
let runInBackground = false
let completed = false
let result: string = ""
let exitDetails: ExitCodeDetails | undefined
Expand All @@ -150,23 +150,9 @@ export async function executeCommand(
const clineProvider = await cline.providerRef.deref()

const callbacks: RooTerminalCallbacks = {
onLine: async (output: string, process: RooTerminalProcess) => {
onLine: async (output: string) => {
const status: CommandExecutionStatus = { executionId, status: "output", output }
clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })

if (runInBackground) {
return
}

try {
const { response, text, images } = await cline.ask("command_output", "")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To expand on this - we changed the way "Continue while command executes" works. It's triggered directly by the webview with a terminalOperation event rather than the ask mechanism. One thing that might have been lost is the ability to provide text to the model when you trigger background execution, but I can fix that.

runInBackground = true

if (response === "messageResponse") {
message = { text, images }
process.continue()
}
} catch (_error) {}
},
onCompleted: (output: string | undefined) => {
result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit)
Expand All @@ -177,6 +163,9 @@ export async function executeCommand(
console.log(`[executeCommand] onShellExecutionStarted: ${pid}`)
const status: CommandExecutionStatus = { executionId, status: "started", pid, command }
clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
// This `command_output` message tells the webview to render the
// appropriate primary and secondary buttons.
cline.say("command_output", "")
},
onShellExecutionComplete: (details: ExitCodeDetails) => {
const status: CommandExecutionStatus = { executionId, status: "exited", exitCode: details.exitCode }
Expand Down Expand Up @@ -216,8 +205,7 @@ export async function executeCommand(
// Wait for a short delay to ensure all messages are sent to the webview.
// This delay allows time for non-awaited promises to be created and
// for their associated messages to be sent to the webview, maintaining
// the correct order of messages (although the webview is smart about
// grouping command_output messages despite any gaps anyways).
// the correct order of messages.
await delay(50)

if (message) {
Expand Down
4 changes: 3 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
await provider.postStateToWebview()
break
case "askResponse":
provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
const instance = provider.getCurrentCline()
console.log("askResponse ->", message, !!instance)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider replacing the direct console.log with a proper logging utility (e.g. provider.log) or remove it before production to avoid exposing internal state.

Suggested change
console.log("askResponse ->", message, !!instance)
provider.log(`askResponse ->`, message, !!instance)

This comment was generated because it violated a code review rule: mrule_OR1S8PRRHcvbdFib.

instance?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break
case "terminalOperation":
if (message.terminalOperation) {
Expand Down
2 changes: 0 additions & 2 deletions src/exports/roo-code.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ type ClineMessage = {
| (
| "followup"
| "command"
| "command_output"
| "completion_result"
| "tool"
| "api_req_failed"
Expand Down Expand Up @@ -381,7 +380,6 @@ type RooCodeEvents = {
| (
| "followup"
| "command"
| "command_output"
| "completion_result"
| "tool"
| "api_req_failed"
Expand Down
2 changes: 0 additions & 2 deletions src/exports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@ type ClineMessage = {
| (
| "followup"
| "command"
| "command_output"
| "completion_result"
| "tool"
| "api_req_failed"
Expand Down Expand Up @@ -390,7 +389,6 @@ type RooCodeEvents = {
| (
| "followup"
| "command"
| "command_output"
| "completion_result"
| "tool"
| "api_req_failed"
Expand Down
1 change: 0 additions & 1 deletion src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,6 @@ export const isGlobalStateKey = (key: string): key is Keys<GlobalState> =>
export const clineAsks = [
"followup",
"command",
"command_output",
"completion_result",
"tool",
"api_req_failed",
Expand Down
7 changes: 0 additions & 7 deletions src/shared/__tests__/combineCommandSequences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,13 @@ const messages: ClineMessage[] = [
},
{ ts: 1745710930748, type: "ask", ask: "command", text: "ping www.google.com", partial: false },
{ ts: 1745710930894, type: "say", say: "command_output", text: "", images: undefined },
{ ts: 1745710930894, type: "ask", ask: "command_output", text: "" },
{
ts: 1745710930954,
type: "say",
say: "command_output",
text: "PING www.google.com (142.251.46.228): 56 data bytes\n",
images: undefined,
},
{
ts: 1745710930954,
type: "ask",
ask: "command_output",
text: "PING www.google.com (142.251.46.228): 56 data bytes\n",
},
]

describe("combineCommandSequences", () => {
Expand Down
4 changes: 2 additions & 2 deletions src/shared/combineCommandSequences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[
break // Stop if we encounter the next command.
}

if (ask === "command_output" || say === "command_output") {
if (say === "command_output") {
if (!previous) {
combinedText += `\n${COMMAND_OUTPUT_STRING}`
}
Expand Down Expand Up @@ -68,7 +68,7 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[
// Second pass: remove command_outputs and replace original commands with
// combined ones.
return messages
.filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
.filter((msg) => msg.say !== "command_output")
.map((msg) => {
if (msg.type === "ask" && msg.ask === "command") {
return combinedCommands.find((cmd) => cmd.ts === msg.ts) || msg
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,7 @@ export const ChatRowContent = ({
{icon}
{title}
</div>
<CommandExecution executionId={message.progressStatus?.id} text={message.text} />
<CommandExecution executionId={message.ts.toString()} text={message.text} />
</>
)
case "use_mcp_server":
Expand Down
40 changes: 24 additions & 16 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const [selectedImages, setSelectedImages] = useState<string[]>([])

// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
const [clineAsk, setClineAsk] = useState<ClineAsk | "command_output" | undefined>(undefined)
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
Expand Down Expand Up @@ -229,13 +229,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setPrimaryButtonText(t("chat:runCommand.title"))
setSecondaryButtonText(t("chat:reject.title"))
break
case "command_output":
setTextAreaDisabled(false)
setClineAsk("command_output")
setEnableButtons(true)
setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
setSecondaryButtonText(t("chat:killCommand.title"))
break
case "use_mcp_server":
if (!isAutoApproved(lastMessage) && !isPartial) {
playSound("notification")
Expand Down Expand Up @@ -289,8 +282,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setTextAreaDisabled(true)
break
case "api_req_started":
if (secondLastMessage?.ask === "command_output") {
// If the last ask is a command_output, and we
if (secondLastMessage?.ask === "command") {
// If the last ask is a command, and we
// receive an api_req_started, then that means
// the command has finished and we don't need
// input from the user anymore (in every other
Expand All @@ -304,12 +297,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setEnableButtons(false)
}
break
case "command_output":
setTextAreaDisabled(false)
setClineAsk("command_output")
setEnableButtons(true)
setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
setSecondaryButtonText(t("chat:killCommand.title"))
break
case "api_req_finished":
case "error":
case "text":
case "browser_action":
case "browser_action_result":
case "command_output":
case "mcp_server_request_started":
case "mcp_server_response":
case "completion_result":
Expand Down Expand Up @@ -399,7 +398,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
case "tool":
case "browser_action_launch":
case "command": // User can provide feedback to a tool or command use.
case "command_output": // User can send input to command stdin.
case "use_mcp_server":
case "completion_result": // If this happens then the user has feedback for the completion result.
case "resume_task":
Expand Down Expand Up @@ -441,6 +439,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()

console.log(`handlePrimaryButtonClick -> clineAsk=${clineAsk}`, text)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove or replace console.log debug statements in production code. Debug logs like this may clutter the console and should be managed with proper logging or removed.

Suggested change
console.log(`handlePrimaryButtonClick -> clineAsk=${clineAsk}`, text)


switch (clineAsk) {
case "api_req_failed":
case "command":
Expand Down Expand Up @@ -521,6 +521,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
break
}

setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
Expand Down Expand Up @@ -749,7 +750,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return alwaysAllowExecute && isAllowedCommand(message)
}

// For read/write operations, check if it's outside workspace and if we have permission for that
// For read/write operations, check if it's outside workspace and
// if we have permission for that.
if (message.ask === "tool") {
let tool: any = {}

Expand Down Expand Up @@ -1114,13 +1116,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}

const autoApprove = async () => {
if (isAutoApproved(lastMessage)) {
if (lastMessage?.ask && isAutoApproved(lastMessage)) {
// Add delay for write operations.
if (lastMessage?.ask === "tool" && isWriteToolAction(lastMessage)) {
if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) {
await new Promise((resolve) => setTimeout(resolve, writeDelayMs))
}

handlePrimaryButtonClick()
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })

setInputValue("")
setSelectedImages([])
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
}
}
autoApprove()
Expand Down
6 changes: 1 addition & 5 deletions webview-ui/src/components/chat/CommandExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { cn } from "@src/lib/utils"
import { Button } from "@src/components/ui"

interface CommandExecutionProps {
executionId?: string
executionId: string
text?: string
}

Expand All @@ -36,10 +36,6 @@ export const CommandExecution = ({ executionId, text }: CommandExecutionProps) =

const onMessage = useCallback(
(event: MessageEvent) => {
if (!executionId) {
return
}

const message: ExtensionMessage = event.data

if (message.type === "commandExecutionStatus") {
Expand Down