Refactor ClineAsk

This commit is contained in:
Saoud Rizwan
2024-10-06 04:24:23 -04:00
parent 7ee0a58f9b
commit d5a998a23a
4 changed files with 40 additions and 40 deletions

View File

@@ -24,14 +24,14 @@ import { combineCommandSequences } from "../shared/combineCommandSequences"
import { import {
ClaudeApiReqCancelReason, ClaudeApiReqCancelReason,
ClaudeApiReqInfo, ClaudeApiReqInfo,
ClaudeAsk, ClineAsk,
ClineMessage, ClineMessage,
ClaudeSay, ClaudeSay,
ClaudeSayTool, ClaudeSayTool,
} from "../shared/ExtensionMessage" } from "../shared/ExtensionMessage"
import { getApiMetrics } from "../shared/getApiMetrics" import { getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem" import { HistoryItem } from "../shared/HistoryItem"
import { ClaudeAskResponse } from "../shared/WebviewMessage" import { ClineAskResponse } from "../shared/WebviewMessage"
import { calculateApiCost } from "../utils/cost" import { calculateApiCost } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs" import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path" import { arePathsEqual, getReadablePath } from "../utils/path"
@@ -60,7 +60,7 @@ export class Cline {
alwaysAllowReadOnly: boolean alwaysAllowReadOnly: boolean
apiConversationHistory: Anthropic.MessageParam[] = [] apiConversationHistory: Anthropic.MessageParam[] = []
claudeMessages: ClineMessage[] = [] claudeMessages: ClineMessage[] = []
private askResponse?: ClaudeAskResponse private askResponse?: ClineAskResponse
private askResponseText?: string private askResponseText?: string
private askResponseImages?: string[] private askResponseImages?: string[]
private lastMessageTs?: number private lastMessageTs?: number
@@ -208,10 +208,10 @@ export class Cline {
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message) // partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
async ask( async ask(
type: ClaudeAsk, type: ClineAsk,
text?: string, text?: string,
partial?: boolean partial?: boolean
): Promise<{ response: ClaudeAskResponse; text?: string; images?: string[] }> { ): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.) // If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
if (this.abort) { if (this.abort) {
throw new Error("Cline instance aborted") throw new Error("Cline instance aborted")
@@ -302,7 +302,7 @@ export class Cline {
return result return result
} }
async handleWebviewAskResponse(askResponse: ClaudeAskResponse, text?: string, images?: string[]) { async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) {
this.askResponse = askResponse this.askResponse = askResponse
this.askResponseText = text this.askResponseText = text
this.askResponseImages = images this.askResponseImages = images
@@ -442,7 +442,7 @@ export class Cline {
// ) // )
// (lastClaudeMessage?.ask === "command" && secondLastClaudeMessage?.ask === "completion_result") // (lastClaudeMessage?.ask === "command" && secondLastClaudeMessage?.ask === "completion_result")
let askType: ClaudeAsk let askType: ClineAsk
if (lastClaudeMessage?.ask === "completion_result") { if (lastClaudeMessage?.ask === "completion_result") {
askType = "resume_completed_task" askType = "resume_completed_task"
} else { } else {
@@ -875,7 +875,7 @@ export class Cline {
} }
} }
const askApproval = async (type: ClaudeAsk, partialMessage?: string) => { const askApproval = async (type: ClineAsk, partialMessage?: string) => {
const { response, text, images } = await this.ask(type, partialMessage, false) const { response, text, images } = await this.ask(type, partialMessage, false)
if (response !== "yesButtonTapped") { if (response !== "yesButtonTapped") {
if (response === "messageResponse") { if (response === "messageResponse") {

View File

@@ -40,14 +40,14 @@ export interface ExtensionState {
export interface ClineMessage { export interface ClineMessage {
ts: number ts: number
type: "ask" | "say" type: "ask" | "say"
ask?: ClaudeAsk ask?: ClineAsk
say?: ClaudeSay say?: ClaudeSay
text?: string text?: string
images?: string[] images?: string[]
partial?: boolean partial?: boolean
} }
export type ClaudeAsk = export type ClineAsk =
| "followup" | "followup"
| "command" | "command"
| "command_output" | "command_output"

View File

@@ -23,10 +23,10 @@ export interface WebviewMessage {
| "cancelTask" | "cancelTask"
| "refreshOpenRouterModels" | "refreshOpenRouterModels"
text?: string text?: string
askResponse?: ClaudeAskResponse askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration apiConfiguration?: ApiConfiguration
images?: string[] images?: string[]
bool?: boolean bool?: boolean
} }
export type ClaudeAskResponse = "yesButtonTapped" | "noButtonTapped" | "messageResponse" export type ClineAskResponse = "yesButtonTapped" | "noButtonTapped" | "messageResponse"

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useDeepCompareEffect, useEvent, useMount } from "react-use" import { useDeepCompareEffect, useEvent, useMount } from "react-use"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso" import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import styled from "styled-components" import styled from "styled-components"
import { ClaudeAsk, ClaudeSayTool, ExtensionMessage } from "../../../../src/shared/ExtensionMessage" import { ClineAsk, ClaudeSayTool, ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { findLast } from "../../../../src/shared/array" import { findLast } from "../../../../src/shared/array"
import { combineApiRequests } from "../../../../src/shared/combineApiRequests" import { combineApiRequests } from "../../../../src/shared/combineApiRequests"
import { combineCommandSequences } from "../../../../src/shared/combineCommandSequences" import { combineCommandSequences } from "../../../../src/shared/combineCommandSequences"
@@ -42,7 +42,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [selectedImages, setSelectedImages] = useState<string[]>([]) 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) // 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 [claudeAsk, setClaudeAsk] = useState<ClaudeAsk | undefined>(undefined) const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
const [enableButtons, setEnableButtons] = useState<boolean>(false) const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined) const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined) const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
@@ -69,28 +69,28 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
switch (lastMessage.ask) { switch (lastMessage.ask) {
case "api_req_failed": case "api_req_failed":
setTextAreaDisabled(true) setTextAreaDisabled(true)
setClaudeAsk("api_req_failed") setClineAsk("api_req_failed")
setEnableButtons(true) setEnableButtons(true)
setPrimaryButtonText("Retry") setPrimaryButtonText("Retry")
setSecondaryButtonText("Start New Task") setSecondaryButtonText("Start New Task")
break break
case "mistake_limit_reached": case "mistake_limit_reached":
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClaudeAsk("mistake_limit_reached") setClineAsk("mistake_limit_reached")
setEnableButtons(true) setEnableButtons(true)
setPrimaryButtonText("Proceed Anyways") setPrimaryButtonText("Proceed Anyways")
setSecondaryButtonText("Start New Task") setSecondaryButtonText("Start New Task")
break break
case "followup": case "followup":
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClaudeAsk("followup") setClineAsk("followup")
setEnableButtons(isPartial) setEnableButtons(isPartial)
// setPrimaryButtonText(undefined) // setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
break break
case "tool": case "tool":
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClaudeAsk("tool") setClineAsk("tool")
setEnableButtons(!isPartial) setEnableButtons(!isPartial)
const tool = JSON.parse(lastMessage.text || "{}") as ClaudeSayTool const tool = JSON.parse(lastMessage.text || "{}") as ClaudeSayTool
switch (tool.tool) { switch (tool.tool) {
@@ -107,14 +107,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break break
case "command": case "command":
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClaudeAsk("command") setClineAsk("command")
setEnableButtons(!isPartial) setEnableButtons(!isPartial)
setPrimaryButtonText("Run Command") setPrimaryButtonText("Run Command")
setSecondaryButtonText("Reject") setSecondaryButtonText("Reject")
break break
case "command_output": case "command_output":
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClaudeAsk("command_output") setClineAsk("command_output")
setEnableButtons(true) setEnableButtons(true)
setPrimaryButtonText("Proceed While Running") setPrimaryButtonText("Proceed While Running")
setSecondaryButtonText(undefined) setSecondaryButtonText(undefined)
@@ -122,14 +122,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "completion_result": case "completion_result":
// extension waiting for feedback. but we can just present a new task button // extension waiting for feedback. but we can just present a new task button
setTextAreaDisabled(isPartial) setTextAreaDisabled(isPartial)
setClaudeAsk("completion_result") setClineAsk("completion_result")
setEnableButtons(!isPartial) setEnableButtons(!isPartial)
setPrimaryButtonText("Start New Task") setPrimaryButtonText("Start New Task")
setSecondaryButtonText(undefined) setSecondaryButtonText(undefined)
break break
case "resume_task": case "resume_task":
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClaudeAsk("resume_task") setClineAsk("resume_task")
setEnableButtons(true) setEnableButtons(true)
setPrimaryButtonText("Resume Task") setPrimaryButtonText("Resume Task")
setSecondaryButtonText(undefined) setSecondaryButtonText(undefined)
@@ -137,7 +137,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break break
case "resume_completed_task": case "resume_completed_task":
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClaudeAsk("resume_completed_task") setClineAsk("resume_completed_task")
setEnableButtons(true) setEnableButtons(true)
setPrimaryButtonText("Start New Task") setPrimaryButtonText("Start New Task")
setSecondaryButtonText(undefined) setSecondaryButtonText(undefined)
@@ -154,7 +154,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setInputValue("") setInputValue("")
setTextAreaDisabled(true) setTextAreaDisabled(true)
setSelectedImages([]) setSelectedImages([])
setClaudeAsk(undefined) setClineAsk(undefined)
setEnableButtons(false) setEnableButtons(false)
} }
break break
@@ -174,7 +174,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// this would get called after sending the first message, so we have to watch messages.length instead // this would get called after sending the first message, so we have to watch messages.length instead
// No messages, so user has to submit a task // No messages, so user has to submit a task
// setTextAreaDisabled(false) // setTextAreaDisabled(false)
// setClaudeAsk(undefined) // setClineAsk(undefined)
// setPrimaryButtonText(undefined) // setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
} }
@@ -183,7 +183,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEffect(() => { useEffect(() => {
if (messages.length === 0) { if (messages.length === 0) {
setTextAreaDisabled(false) setTextAreaDisabled(false)
setClaudeAsk(undefined) setClineAsk(undefined)
setEnableButtons(false) setEnableButtons(false)
setPrimaryButtonText(undefined) setPrimaryButtonText(undefined)
setSecondaryButtonText(undefined) setSecondaryButtonText(undefined)
@@ -191,9 +191,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}, [messages.length]) }, [messages.length])
const isStreaming = useMemo(() => { const isStreaming = useMemo(() => {
const isLastAsk = !!modifiedMessages.at(-1)?.ask // checking claudeAsk isn't enough since messages effect may be called again for a tool for example, set claudeAsk to its value, and if the next message is not an ask then it doesn't reset. This is likely due to how much more often we're updating messages as compared to before, and should be resolved with optimizations as it's likely a rendering bug. but as a final guard for now, the cancel button will show if the last message is not an ask const isLastAsk = !!modifiedMessages.at(-1)?.ask // checking clineAsk isn't enough since messages effect may be called again for a tool for example, set clineAsk to its value, and if the next message is not an ask then it doesn't reset. This is likely due to how much more often we're updating messages as compared to before, and should be resolved with optimizations as it's likely a rendering bug. but as a final guard for now, the cancel button will show if the last message is not an ask
const isToolCurrentlyAsking = const isToolCurrentlyAsking =
isLastAsk && claudeAsk !== undefined && enableButtons && primaryButtonText !== undefined isLastAsk && clineAsk !== undefined && enableButtons && primaryButtonText !== undefined
if (isToolCurrentlyAsking) { if (isToolCurrentlyAsking) {
return false return false
} }
@@ -213,7 +213,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
} }
return false return false
}, [modifiedMessages, claudeAsk, enableButtons, primaryButtonText]) }, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
const handleSendMessage = useCallback( const handleSendMessage = useCallback(
(text: string, images: string[]) => { (text: string, images: string[]) => {
@@ -221,8 +221,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
if (text || images.length > 0) { if (text || images.length > 0) {
if (messages.length === 0) { if (messages.length === 0) {
vscode.postMessage({ type: "newTask", text, images }) vscode.postMessage({ type: "newTask", text, images })
} else if (claudeAsk) { } else if (clineAsk) {
switch (claudeAsk) { switch (clineAsk) {
case "followup": case "followup":
case "tool": case "tool":
case "command": // user can provide feedback to a tool or command use case "command": // user can provide feedback to a tool or command use
@@ -244,14 +244,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setInputValue("") setInputValue("")
setTextAreaDisabled(true) setTextAreaDisabled(true)
setSelectedImages([]) setSelectedImages([])
setClaudeAsk(undefined) setClineAsk(undefined)
setEnableButtons(false) setEnableButtons(false)
// setPrimaryButtonText(undefined) // setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false disableAutoScrollRef.current = false
} }
}, },
[messages.length, claudeAsk] [messages.length, clineAsk]
) )
const startNewTask = useCallback(() => { const startNewTask = useCallback(() => {
@@ -259,10 +259,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}, []) }, [])
/* /*
This logic depends on the useEffect[messages] above to set claudeAsk, after which buttons are shown and we then send an askResponse to the extension. This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
*/ */
const handlePrimaryButtonClick = useCallback(() => { const handlePrimaryButtonClick = useCallback(() => {
switch (claudeAsk) { switch (clineAsk) {
case "api_req_failed": case "api_req_failed":
case "command": case "command":
case "command_output": case "command_output":
@@ -278,11 +278,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break break
} }
setTextAreaDisabled(true) setTextAreaDisabled(true)
setClaudeAsk(undefined) setClineAsk(undefined)
setEnableButtons(false) setEnableButtons(false)
// setPrimaryButtonText(undefined) // setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
}, [claudeAsk, startNewTask]) }, [clineAsk, startNewTask])
const handleSecondaryButtonClick = useCallback(() => { const handleSecondaryButtonClick = useCallback(() => {
if (isStreaming) { if (isStreaming) {
@@ -291,7 +291,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return return
} }
switch (claudeAsk) { switch (clineAsk) {
case "api_req_failed": case "api_req_failed":
case "mistake_limit_reached": case "mistake_limit_reached":
startNewTask() startNewTask()
@@ -303,11 +303,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break break
} }
setTextAreaDisabled(true) setTextAreaDisabled(true)
setClaudeAsk(undefined) setClineAsk(undefined)
setEnableButtons(false) setEnableButtons(false)
// setPrimaryButtonText(undefined) // setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined) // setSecondaryButtonText(undefined)
}, [claudeAsk, startNewTask, isStreaming]) }, [clineAsk, startNewTask, isStreaming])
const handleTaskCloseButtonClick = useCallback(() => { const handleTaskCloseButtonClick = useCallback(() => {
startNewTask() startNewTask()