Skip to content

Commit 4a871c6

Browse files
committed
UI progress
1 parent 411e1ff commit 4a871c6

File tree

14 files changed

+462
-81
lines changed

14 files changed

+462
-81
lines changed

src/core/Cline.ts

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import cloneDeep from "clone-deep"
3-
import { DiffStrategy, getDiffStrategy, UnifiedDiffStrategy } from "./diff/DiffStrategy"
3+
import { DiffStrategy, getDiffStrategy } from "./diff/DiffStrategy"
44
import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validator"
55
import delay from "delay"
66
import fs from "fs/promises"
@@ -9,9 +9,10 @@ import pWaitFor from "p-wait-for"
99
import * as path from "path"
1010
import { serializeError } from "serialize-error"
1111
import * as vscode from "vscode"
12-
import { ApiHandler, SingleCompletionHandler, buildApiHandler } from "../api"
12+
import { ApiHandler, buildApiHandler } from "../api"
1313
import { ApiStream } from "../api/transform/stream"
1414
import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
15+
import { LocalCheckpointer } from "../integrations/checkpoints/LocalCheckpointer"
1516
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
1617
import {
1718
extractTextFromFile,
@@ -52,12 +53,11 @@ import { parseMentions } from "./mentions"
5253
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
5354
import { formatResponse } from "./prompts/responses"
5455
import { SYSTEM_PROMPT } from "./prompts/system"
55-
import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
56+
import { defaultModeSlug, getModeBySlug } from "../shared/modes"
5657
import { truncateHalfConversation } from "./sliding-window"
5758
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
5859
import { detectCodeOmission } from "../integrations/editor/detect-omission"
5960
import { BrowserSession } from "../services/browser/BrowserSession"
60-
import { OpenRouterHandler } from "../api/providers/openrouter"
6161
import { McpHub } from "../services/mcp/McpHub"
6262
import crypto from "crypto"
6363
import { insertGroups } from "./diff/insert-groups"
@@ -96,6 +96,8 @@ export class Cline {
9696
didFinishAborting = false
9797
abandoned = false
9898
private diffViewProvider: DiffViewProvider
99+
checkpointsEnabled: boolean = false
100+
private checkpointer?: LocalCheckpointer
99101

100102
// streaming
101103
private currentStreamingContentIndex = 0
@@ -113,6 +115,7 @@ export class Cline {
113115
apiConfiguration: ApiConfiguration,
114116
customInstructions?: string,
115117
enableDiff?: boolean,
118+
enableCheckpoints?: boolean,
116119
fuzzyMatchThreshold?: number,
117120
task?: string | undefined,
118121
images?: string[] | undefined,
@@ -133,6 +136,8 @@ export class Cline {
133136
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
134137
this.providerRef = new WeakRef(provider)
135138
this.diffViewProvider = new DiffViewProvider(cwd)
139+
this.checkpointsEnabled = enableCheckpoints ?? false
140+
this.checkpointer = undefined
136141

137142
if (historyItem) {
138143
this.taskId = historyItem.id
@@ -257,13 +262,20 @@ export class Cline {
257262

258263
// Communicate with webview
259264

260-
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
265+
// partial has three valid states
266+
// true (partial message),
267+
// false (completion of partial message),
268+
// undefined (individual complete message)
261269
async ask(
262270
type: ClineAsk,
263271
text?: string,
264272
partial?: boolean,
265273
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
266-
// 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.)
274+
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise
275+
// still running in the background, in which case we don't want to send its result to the webview as it is
276+
// attached to a new instance of Cline now. So we can safely ignore the result of any active promises, an
277+
// this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the
278+
// reference to this instance, but the instance is still alive until this promise resolves or rejects.)
267279
if (this.abort) {
268280
throw new Error("Roo Code instance aborted")
269281
}
@@ -812,7 +824,9 @@ export class Cline {
812824

813825
const { browserViewportSize, mode, customModePrompts, preferredLanguage, experiments } =
814826
(await this.providerRef.deref()?.getState()) ?? {}
827+
815828
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
829+
816830
const systemPrompt = await (async () => {
817831
const provider = this.providerRef.deref()
818832
if (!provider) {
@@ -884,7 +898,10 @@ export class Cline {
884898
const firstChunk = await iterator.next()
885899
yield firstChunk.value
886900
} catch (error) {
887-
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
901+
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't
902+
// streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry
903+
// button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may
904+
// have executed, so that error is handled differently and requires cancelling the task entirely.
888905
if (alwaysApproveResubmit) {
889906
const errorMsg = error.message ?? "Unknown error"
890907
const requestDelay = requestDelaySeconds || 5
@@ -920,8 +937,10 @@ export class Cline {
920937
}
921938

922939
// no error, so we can continue to yield all remaining chunks
923-
// (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)
924-
// 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.
940+
// (needs to be placed outside of try/catch since it we want caller to handle errors not with api_req_failed
941+
// as that is reserved for first chunk failures only)
942+
// this delegates to another generator or iterable object. In this case, it's saying "yield all remaining values
943+
// from this iterator". This effectively passes along all subsequent chunks from the original stream.
925944
yield* iterator
926945
}
927946

@@ -934,6 +953,7 @@ export class Cline {
934953
this.presentAssistantMessageHasPendingUpdates = true
935954
return
936955
}
956+
937957
this.presentAssistantMessageLocked = true
938958
this.presentAssistantMessageHasPendingUpdates = false
939959

@@ -945,10 +965,12 @@ export class Cline {
945965
// console.log("no more content blocks to stream! this shouldn't happen?")
946966
this.presentAssistantMessageLocked = false
947967
return
948-
//throw new Error("No more content blocks to stream! This shouldn't happen...") // remove and just return after testing
968+
// remove and just return after testing
969+
// throw new Error("No more content blocks to stream! This shouldn't happen...")
949970
}
950971

951972
const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
973+
952974
switch (block.type) {
953975
case "text": {
954976
if (this.didRejectTool || this.didAlreadyUseTool) {
@@ -1072,7 +1094,7 @@ export class Cline {
10721094
} else {
10731095
this.userMessageContent.push(...content)
10741096
}
1075-
// once a tool result has been collected, ignore all other tool uses since we should only ever present one tool result per message
1097+
// once a tool result has been collected, ignore all other tool uses since we should only everpresent one tool result per message
10761098
this.didAlreadyUseTool = true
10771099
}
10781100

@@ -1152,6 +1174,7 @@ export class Cline {
11521174

11531175
// Validate tool use before execution
11541176
const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {}
1177+
11551178
try {
11561179
validateToolUse(
11571180
block.name as ToolName,
@@ -1360,6 +1383,7 @@ export class Cline {
13601383
break
13611384
}
13621385
}
1386+
13631387
case "apply_diff": {
13641388
const relPath: string | undefined = block.params.path
13651389
const diffContent: string | undefined = block.params.diff
@@ -1842,6 +1866,7 @@ export class Cline {
18421866
break
18431867
}
18441868
}
1869+
18451870
case "list_files": {
18461871
const relDirPath: string | undefined = block.params.path
18471872
const recursiveRaw: string | undefined = block.params.recursive
@@ -1884,6 +1909,7 @@ export class Cline {
18841909
break
18851910
}
18861911
}
1912+
18871913
case "list_code_definition_names": {
18881914
const relDirPath: string | undefined = block.params.path
18891915
const sharedMessageProps: ClineSayTool = {
@@ -1925,6 +1951,7 @@ export class Cline {
19251951
break
19261952
}
19271953
}
1954+
19281955
case "search_files": {
19291956
const relDirPath: string | undefined = block.params.path
19301957
const regex: string | undefined = block.params.regex
@@ -1973,6 +2000,7 @@ export class Cline {
19732000
break
19742001
}
19752002
}
2003+
19762004
case "browser_action": {
19772005
const action: BrowserAction | undefined = block.params.action as BrowserAction
19782006
const url: string | undefined = block.params.url
@@ -2119,6 +2147,7 @@ export class Cline {
21192147
break
21202148
}
21212149
}
2150+
21222151
case "execute_command": {
21232152
const command: string | undefined = block.params.command
21242153
try {
@@ -2153,6 +2182,7 @@ export class Cline {
21532182
break
21542183
}
21552184
}
2185+
21562186
case "use_mcp_tool": {
21572187
const server_name: string | undefined = block.params.server_name
21582188
const tool_name: string | undefined = block.params.tool_name
@@ -2248,6 +2278,7 @@ export class Cline {
22482278
break
22492279
}
22502280
}
2281+
22512282
case "access_mcp_resource": {
22522283
const server_name: string | undefined = block.params.server_name
22532284
const uri: string | undefined = block.params.uri
@@ -2309,6 +2340,7 @@ export class Cline {
23092340
break
23102341
}
23112342
}
2343+
23122344
case "ask_followup_question": {
23132345
const question: string | undefined = block.params.question
23142346
try {
@@ -2336,6 +2368,7 @@ export class Cline {
23362368
break
23372369
}
23382370
}
2371+
23392372
case "switch_mode": {
23402373
const mode_slug: string | undefined = block.params.mode_slug
23412374
const reason: string | undefined = block.params.reason
@@ -2536,6 +2569,7 @@ export class Cline {
25362569
}
25372570
}
25382571
}
2572+
25392573
break
25402574
}
25412575

@@ -2544,6 +2578,7 @@ export class Cline {
25442578
When you see the UI inactive during this, it means that a tool is breaking without presenting any UI. For example the write_to_file tool was breaking when relpath was undefined, and for invalid relpath it never presented UI.
25452579
*/
25462580
this.presentAssistantMessageLocked = false // this needs to be placed here, if not then calling this.presentAssistantMessage below would fail (sometimes) since it's locked
2581+
25472582
// NOTE: when tool is rejected, iterator stream is interrupted and it waits for userMessageContentReady to be true. Future calls to present will skip execution since didRejectTool and iterate until contentIndex is set to message length and it sets userMessageContentReady to true itself (instead of preemptively doing it in iterator)
25482583
if (!block.partial || this.didRejectTool || this.didAlreadyUseTool) {
25492584
// block is finished streaming and executing
@@ -2602,7 +2637,8 @@ export class Cline {
26022637
// get previous api req's index to check token usage and determine if we need to truncate conversation history
26032638
const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
26042639

2605-
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
2640+
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project
2641+
// which for large projects can take a few seconds
26062642
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
26072643
await this.say(
26082644
"api_req_started",
@@ -2619,7 +2655,8 @@ export class Cline {
26192655

26202656
await this.addToApiConversationHistory({ role: "user", content: userContent })
26212657

2622-
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
2658+
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start
2659+
// the API request (to load potential details for example), we need to update the text of that message
26232660
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
26242661
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
26252662
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
@@ -2634,9 +2671,10 @@ export class Cline {
26342671
let outputTokens = 0
26352672
let totalCost: number | undefined
26362673

2637-
// update api_req_started. we can't use api_req_finished anymore since it's a unique case where it could come after a streaming message (ie in the middle of being updated or executed)
2638-
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
2639-
// (it's worth removing a few months from now)
2674+
// update api_req_started. we can't use api_req_finished anymore since it's a unique case where it could
2675+
// come after a streaming message (ie in the middle of being updated or executed)
2676+
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy
2677+
// purposes to keep track of prices in tasks from history (it's worth removing a few months from now)
26402678
const updateApiReqMsg = (cancelReason?: ClineApiReqCancelReason, streamingFailedMessage?: string) => {
26412679
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
26422680
...JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}"),
@@ -2710,7 +2748,9 @@ export class Cline {
27102748
this.presentAssistantMessageHasPendingUpdates = false
27112749
await this.diffViewProvider.reset()
27122750

2713-
const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)
2751+
// yields only if the first chunk is successful, otherwise will allow the user to retry the request (most
2752+
// likely due to rate limit error, which gets thrown on the first chunk)
2753+
const stream = this.attemptApiRequest(previousApiReqIndex)
27142754
let assistantMessage = ""
27152755
let reasoningMessage = ""
27162756
try {

0 commit comments

Comments
 (0)