Compare commits

...
Author SHA1 Message Date
Saoud Rizwan 2cdee1d0f0 Start task header not expanded 2025-03-12 21:55:47 -07:00
Saoud Rizwan ab4f7a2703 Fixes 2025-03-12 21:53:46 -07:00
Saoud Rizwan 737a433903 Add plan mode options 2025-03-12 16:52:57 -07:00
Saoud Rizwan 680c57d951 Fix taskheader for anthropic only showing cache info after first request 2025-03-12 14:30:48 -07:00
Saoud Rizwan 778587b049 Fixes 2025-03-12 14:27:30 -07:00
Saoud Rizwan c1e4b08a09 Add question options 2025-03-12 14:22:37 -07:00
10 changed files with 275 additions and 121 deletions
+55 -13
View File
@@ -22,7 +22,7 @@ import { listFiles } from "../services/glob/list-files"
import { regexSearchFiles } from "../services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { ApiConfiguration } from "../shared/api"
import { findLast, findLastIndex } from "../shared/array"
import { findLast, findLastIndex, parsePartialArrayString } from "../shared/array"
import { AutoApprovalSettings } from "../shared/AutoApprovalSettings"
import { BrowserSettings } from "../shared/BrowserSettings"
import { ChatSettings } from "../shared/ChatSettings"
@@ -35,8 +35,10 @@ import {
ClineApiReqCancelReason,
ClineApiReqInfo,
ClineAsk,
ClineAskQuestion,
ClineAskUseMcpServer,
ClineMessage,
ClinePlanModeResponse,
ClineSay,
ClineSayBrowserAction,
ClineSayTool,
@@ -2720,9 +2722,14 @@ export class Cline {
}
case "ask_followup_question": {
const question: string | undefined = block.params.question
const optionsRaw: string | undefined = block.params.options
const sharedMessage = {
question: removeClosingTag("question", question),
options: parsePartialArrayString(removeClosingTag("options", optionsRaw)),
} satisfies ClineAskQuestion
try {
if (block.partial) {
await this.ask("followup", removeClosingTag("question", question), block.partial).catch(() => {})
await this.ask("followup", JSON.stringify(sharedMessage), block.partial).catch(() => {})
break
} else {
if (!question) {
@@ -2740,8 +2747,25 @@ export class Cline {
})
}
const { text, images } = await this.ask("followup", question, false)
await this.say("user_feedback", text ?? "", images)
const { text, images } = await this.ask("followup", JSON.stringify(sharedMessage), false)
// Check if options contains the text response
if (optionsRaw && text && parsePartialArrayString(optionsRaw).includes(text)) {
// Valid option selected, don't show user message in UI
// Update last followup message with selected option
const lastFollowupMessage = findLast(this.clineMessages, (m) => m.ask === "followup")
if (lastFollowupMessage) {
lastFollowupMessage.text = JSON.stringify({
...sharedMessage,
selected: text,
} satisfies ClineAskQuestion)
await this.saveClineMessages()
}
} else {
// Option not selected, send user feedback
await this.say("user_feedback", text ?? "", images)
}
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
break
@@ -2754,11 +2778,14 @@ export class Cline {
}
case "plan_mode_response": {
const response: string | undefined = block.params.response
const optionsRaw: string | undefined = block.params.options
const sharedMessage = {
response: removeClosingTag("response", response),
options: parsePartialArrayString(removeClosingTag("options", optionsRaw)),
} satisfies ClinePlanModeResponse
try {
if (block.partial) {
await this.ask("plan_mode_response", removeClosingTag("response", response), block.partial).catch(
() => {},
)
await this.ask("plan_mode_response", JSON.stringify(sharedMessage), block.partial).catch(() => {})
break
} else {
if (!response) {
@@ -2777,7 +2804,7 @@ export class Cline {
// }
this.isAwaitingPlanResponse = true
let { text, images } = await this.ask("plan_mode_response", response, false)
let { text, images } = await this.ask("plan_mode_response", JSON.stringify(sharedMessage), false)
this.isAwaitingPlanResponse = false
// webview invoke sendMessage will send this marker in order to put webview into the proper state (responding to an ask) and as a flag to extension that the user switched to ACT mode.
@@ -2785,6 +2812,25 @@ export class Cline {
text = ""
}
// Check if options contains the text response
if (optionsRaw && text && parsePartialArrayString(optionsRaw).includes(text)) {
// Valid option selected, don't show user message in UI
// Update last followup message with selected option
const lastPlanMessage = findLast(this.clineMessages, (m) => m.ask === "plan_mode_response")
if (lastPlanMessage) {
lastPlanMessage.text = JSON.stringify({
...sharedMessage,
selected: text,
} satisfies ClinePlanModeResponse)
await this.saveClineMessages()
}
} else {
// Option not selected, send user feedback
if (text || images?.length) {
await this.say("user_feedback", text ?? "", images)
}
}
if (this.didRespondToPlanAskBySwitchingMode) {
pushToolResult(
formatResponse.toolResult(
@@ -2800,10 +2846,6 @@ export class Cline {
pushToolResult(formatResponse.toolResult(`<user_message>\n${text}\n</user_message>`, images))
}
if (text || images?.length) {
await this.say("user_feedback", text ?? "", images)
}
//
break
}
@@ -3563,7 +3605,7 @@ export class Cline {
details +=
"\nIn this mode you should focus on information gathering, asking questions, and architecting a solution. Once you have a plan, use the plan_mode_response tool to engage in a conversational back and forth with the user. Do not use the plan_mode_response tool until you've gathered all the information you need e.g. with read_file or ask_followup_question."
details +=
'\n(Remember: If it seems the user wants you to use tools only available in Act Mode, you should ask the user to "toggle to Act mode" (use those words) - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to Act Mode yourself, and must wait for the user to do it themselves once they are satisfied with the plan.)'
'\n(Remember: If it seems the user wants you to use tools only available in Act Mode, you should ask the user to "toggle to Act mode" (use those words) - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to Act Mode yourself, and must wait for the user to do it themselves once they are satisfied with the plan. You also cannot present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.)'
} else {
details += "\nACT MODE"
}
+1 -61
View File
@@ -45,6 +45,7 @@ export const toolParamNames = [
"arguments",
"uri",
"question",
"options",
"response",
"result",
] as const
@@ -58,64 +59,3 @@ export interface ToolUse {
params: Partial<Record<ToolParamName, string>>
partial: boolean
}
export interface ExecuteCommandToolUse extends ToolUse {
name: "execute_command"
// Pick<Record<ToolParamName, string>, "command"> makes "command" required, but Partial<> makes it optional
params: Partial<Pick<Record<ToolParamName, string>, "command" | "requires_approval">>
}
export interface ReadFileToolUse extends ToolUse {
name: "read_file"
params: Partial<Pick<Record<ToolParamName, string>, "path">>
}
export interface WriteToFileToolUse extends ToolUse {
name: "write_to_file"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "content">>
}
export interface ReplaceInFileToolUse extends ToolUse {
name: "replace_in_file"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "diff">>
}
export interface SearchFilesToolUse extends ToolUse {
name: "search_files"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "regex" | "file_pattern">>
}
export interface ListFilesToolUse extends ToolUse {
name: "list_files"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "recursive">>
}
export interface ListCodeDefinitionNamesToolUse extends ToolUse {
name: "list_code_definition_names"
params: Partial<Pick<Record<ToolParamName, string>, "path">>
}
export interface BrowserActionToolUse extends ToolUse {
name: "browser_action"
params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text">>
}
export interface UseMcpToolToolUse extends ToolUse {
name: "use_mcp_tool"
params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "tool_name" | "arguments">>
}
export interface AccessMcpResourceToolUse extends ToolUse {
name: "access_mcp_resource"
params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "uri">>
}
export interface AskFollowupQuestionToolUse extends ToolUse {
name: "ask_followup_question"
params: Partial<Pick<Record<ToolParamName, string>, "question">>
}
export interface AttemptCompletionToolUse extends ToolUse {
name: "attempt_completion"
params: Partial<Pick<Record<ToolParamName, string>, "result" | "command">>
}
+11 -3
View File
@@ -216,9 +216,13 @@ Usage:
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible answer. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually.
Usage:
<ask_followup_question>
<question>Your question here</question>
<options>
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
</options>
</ask_followup_question>
## attempt_completion
@@ -239,9 +243,13 @@ Your final result description here
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution.
Parameters:
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response.
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible choice or path forward in the planning process. This can help guide the discussion and make it easier for the user to provide input on key decisions. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. Do NOT present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.
Usage:
<plan_mode_response>
<response>Your response here</response>
<options>
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
</options>
</plan_mode_response>
# Tool Use Examples
@@ -813,7 +821,7 @@ You have access to two tools for working with files: **write_to_file** and **rep
## Important Considerations
- Using write_to_file requires providing the files complete final content.
- Using write_to_file requires providing the file's complete final content.
- If you only need to make small changes to an existing file, consider using replace_in_file instead to avoid unnecessarily rewriting the entire file.
- While write_to_file should not be your default choice, don't hesitate to use it when the situation truly calls for it.
@@ -826,12 +834,12 @@ You have access to two tools for working with files: **write_to_file** and **rep
## When to Use
- Small, localized changes like updating a few lines, function implementations, changing variable names, modifying a section of text, etc.
- Targeted improvements where only specific portions of the files content needs to be altered.
- Targeted improvements where only specific portions of the file's content needs to be altered.
- Especially useful for long files where much of the file will remain unchanged.
## Advantages
- More efficient for minor edits, since you dont need to supply the entire file content.
- More efficient for minor edits, since you don't need to supply the entire file content.
- Reduces the chance of errors that can occur when overwriting large files.
# Choosing the Appropriate Tool
+7
View File
@@ -572,6 +572,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
}
break
case "optionsResponse":
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: message.text,
})
break
// case "relaunchChromeDebugMode":
// if (this.cline) {
// this.cline.browserSession.relaunchChromeDebugMode()
+12
View File
@@ -199,6 +199,18 @@ export interface ClineAskUseMcpServer {
uri?: string
}
export interface ClinePlanModeResponse {
response: string
options?: string[]
selected?: string
}
export interface ClineAskQuestion {
question: string
options?: string[]
selected?: string
}
export interface ClineApiReqInfo {
request?: string
tokensIn?: number
+1
View File
@@ -61,6 +61,7 @@ export interface WebviewMessage {
| "invoke"
| "updateSettings"
| "clearAllTaskHistory"
| "optionsResponse"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
+34
View File
@@ -20,3 +20,37 @@ export function findLast<T>(array: Array<T>, predicate: (value: T, index: number
const index = findLastIndex(array, predicate)
return index === -1 ? undefined : array[index]
}
/**
* Converts a partial or complete stringified array into an actual array.
* Handles both complete JSON strings and incomplete array strings.
* Splits on the specific tokens: [" ", " "]
* @param arrayString A string representation of an array, which may be incomplete
* @returns Array of strings parsed from the input
*/
export function parsePartialArrayString(arrayString: string): string[] {
try {
// Try parsing as complete JSON first
return JSON.parse(arrayString)
} catch {
// If JSON parsing fails, handle as partial string
const trimmed = arrayString.trim()
if (!trimmed.startsWith('["')) {
return []
}
// Remove leading ["
let content = trimmed.slice(2)
// Remove trailing "] if it exists
content = content.replace(/"]$/, "")
if (!content) {
return []
}
// Split on ", " token and handle the parts
return content
.split('", "')
.map((item) => item.trim())
.filter(Boolean)
}
}
+44 -6
View File
@@ -5,8 +5,10 @@ import { useEvent, useSize } from "react-use"
import styled from "styled-components"
import {
ClineApiReqInfo,
ClineAskQuestion,
ClineAskUseMcpServer,
ClineMessage,
ClinePlanModeResponse,
ClineSayTool,
COMPLETION_RESULT_CHANGES_FLAG,
ExtensionMessage,
@@ -15,6 +17,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "../../../../src/s
import { useExtensionState } from "../../context/ExtensionStateContext"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import { CheckmarkControl } from "../common/CheckmarkControl"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
@@ -23,10 +26,9 @@ import SuccessButton from "../common/SuccessButton"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
import { highlightMentions } from "./TaskHeader"
import CreditLimitError from "./CreditLimitError"
import { CheckmarkControl } from "../common/CheckmarkControl"
import McpResponseDisplay from "../mcp/McpResponseDisplay"
import { OptionsButtons } from "./OptionsButtons"
import { highlightMentions } from "./TaskHeader"
const ChatRowContainer = styled.div`
padding: 10px 6px 10px 15px;
@@ -1198,6 +1200,19 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
return null // Don't render anything when we get a completion_result ask without text
}
case "followup":
let question: string | undefined
let options: string[] | undefined
let selected: string | undefined
try {
const parsedMessage = JSON.parse(message.text || "{}") as ClineAskQuestion
question = parsedMessage.question
options = parsedMessage.options
selected = parsedMessage.selected
} catch (e) {
// legacy messages would pass question directly
question = message.text
}
return (
<>
{title && (
@@ -1207,16 +1222,39 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
</div>
)}
<div style={{ paddingTop: 10 }}>
<Markdown markdown={message.text} />
<Markdown markdown={question} />
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "followup"}
/>
</div>
</>
)
case "plan_mode_response":
case "plan_mode_response": {
let response: string | undefined
let options: string[] | undefined
let selected: string | undefined
try {
const parsedMessage = JSON.parse(message.text || "{}") as ClinePlanModeResponse
response = parsedMessage.response
options = parsedMessage.options
selected = parsedMessage.selected
} catch (e) {
// legacy messages would pass response directly
response = message.text
}
return (
<div style={{}}>
<Markdown markdown={message.text} />
<Markdown markdown={response} />
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_response"}
/>
</div>
)
}
default:
return null
}
@@ -0,0 +1,69 @@
import styled from "styled-components"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { vscode } from "../../utils/vscode"
const OptionButton = styled.button<{ isSelected?: boolean; isNotSelectable?: boolean }>`
padding: 8px 12px;
background: ${(props) => (props.isSelected ? "var(--vscode-focusBorder)" : CODE_BLOCK_BG_COLOR)};
color: ${(props) => (props.isSelected ? "white" : "var(--vscode-input-foreground)")};
border: 1px solid var(--vscode-editorGroup-border);
border-radius: 2px;
cursor: ${(props) => (props.isNotSelectable ? "default" : "pointer")};
text-align: left;
font-size: 12px;
${(props) =>
!props.isNotSelectable &&
`
&:hover {
background: var(--vscode-focusBorder);
color: white;
}
`}
`
export const OptionsButtons = ({
options,
selected,
isActive,
}: {
options?: string[]
selected?: string
isActive?: boolean
}) => {
if (!options?.length) return null
const hasSelected = selected !== undefined && options.includes(selected)
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
paddingTop: 15,
// marginTop: "22px",
}}>
{/* <div style={{ color: "var(--vscode-descriptionForeground)", fontSize: "11px", textTransform: "uppercase" }}>
SELECT ONE:
</div> */}
{options.map((option, index) => (
<OptionButton
key={index}
isSelected={option === selected}
isNotSelectable={hasSelected || !isActive}
onClick={() => {
if (hasSelected || !isActive) {
return
}
vscode.postMessage({
type: "optionsResponse",
text: option,
})
}}>
{option}
</OptionButton>
))}
</div>
)
}
+41 -38
View File
@@ -34,7 +34,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
onClose,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
const [isTextExpanded, setIsTextExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
@@ -386,49 +386,52 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)}
</div>
{shouldShowPromptCacheInfo && (cacheReads !== undefined || cacheWrites !== undefined) && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
{shouldShowPromptCacheInfo &&
(cacheReads !== undefined ||
cacheWrites !== undefined ||
apiConfiguration?.apiProvider === "anthropic") && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
gap: "4px",
flexWrap: "wrap",
}}>
<i
className="codicon codicon-database"
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(cacheWrites || 0)}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-database"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(cacheWrites || 0)}
</span>
<span
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
{ContextWindowComponent}
{isCostAvailable && (
<div