Compare commits

...
Author SHA1 Message Date
pashpashpashandCline Evaluation 01178909ee adding unique remote git urls to context on first message env variables (#4622)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 15:15:10 -07:00
Sarah Fortune c982216113 Cleanup: Remove unused property and param postMessage from ClineAccountService (#4620)
* Remove unused property and param postMessage

* Remove unused property and param postMessage

Remove unused import.
2025-07-01 14:44:41 -07:00
Sarah Fortune da6f705df2 Remove unused file get-python-env.ts (#4604) 2025-07-01 14:06:14 -07:00
AraandCline Evaluation c7548a7f52 Fixing Bugs in ChatView with Primary/Secondary Buttons and related issues (#4553)
* Splitting chat view into multiple modular files

* Adding Comments and removing redundancies

* Fixing Bugs in ChatView with Primary/Secondary Buttons and related issues

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 12:58:18 -07:00
Tomás Barreiro e5f78a0456 Do not read auth variables from the user env when using Claude Code (#4591) 2025-07-01 12:09:58 +05:30
16 changed files with 180 additions and 156 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: Do not read auth variables from the user env when using Claude Code
+4 -7
View File
@@ -78,13 +78,10 @@ export class Controller {
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
this.accountService = new ClineAccountService(
(msg) => this.postMessageToWebview(msg),
async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
},
)
this.accountService = new ClineAccountService(async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
})
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
+3
View File
@@ -35,6 +35,9 @@ Otherwise, if you have not completed the task and do not need additional informa
tooManyMistakes: (feedback?: string) =>
`You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
autoApprovalMaxReached: (feedback?: string) =>
`Auto-approval limit reached. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
missingToolParameterError: (paramName: string) =>
`Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${toolUseInstructionsReminder}`,
+42 -2
View File
@@ -35,6 +35,7 @@ import pTimeout from "p-timeout"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { getGitRemoteUrls } from "@utils/git"
import { parseAssistantMessageV2, parseAssistantMessageV3, ToolUseName } from "@core/assistant-message"
import {
@@ -1436,7 +1437,10 @@ export class Task {
try {
const { response, text, images, files } = await this.ask("command_output", chunk)
if (response === "yesButtonClicked") {
// proceed while running
// proceed while running - but still capture user feedback if provided
if (text || (images && images.length > 0) || (files && files.length > 0)) {
userFeedback = { text, images, files }
}
} else {
userFeedback = { text, images, files }
}
@@ -1927,12 +1931,42 @@ export class Task {
message: `Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests.`,
})
}
await this.ask(
const { response, text, images, files } = await this.ask(
"auto_approval_max_req_reached",
`Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests. Would you like to reset the count and proceed with the task?`,
)
// if we get past the promise it means the user approved and did not start a new task
this.taskState.consecutiveAutoApprovedRequestsCount = 0
// Process user feedback if provided
if (response === "messageResponse") {
// Display the user's message in the chat UI
await this.say("user_feedback", text, images, files)
// This userContent is for the *next* API call.
const feedbackUserContent: UserContent = []
feedbackUserContent.push({
type: "text",
text: formatResponse.autoApprovalMaxReached(text),
})
if (images && images.length > 0) {
feedbackUserContent.push(...formatResponse.imageBlocks(images))
}
let fileContentString = ""
if (files && files.length > 0) {
fileContentString = await processFilesIntoText(files)
}
if (fileContentString) {
feedbackUserContent.push({
type: "text",
text: fileContentString,
})
}
userContent = feedbackUserContent
}
}
// get previous api req's index to check token usage and determine if we need to truncate conversation history
@@ -2547,6 +2581,12 @@ export class Task {
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.clineIgnoreController)
details += result
}
// Add git remote URLs section
const gitRemotes = await getGitRemoteUrls(cwd)
if (gitRemotes.length > 0) {
details += `\n\n# Git Remote URLs\n${gitRemotes.join("\n")}`
}
}
// Add context window usage information
+11 -5
View File
@@ -128,15 +128,21 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
args.push("--model", modelId)
}
const env: NodeJS.ProcessEnv = {
...process.env,
// The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it.
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
}
// We don't want to consume the user's ANTHROPIC_API_KEY,
// and will allow Claude Code to resolve auth by itself
delete env["ANTHROPIC_API_KEY"]
const claudeCodeProcess = execa(claudePath, args, {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
// The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it.
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
},
env,
cwd,
maxBuffer: 1024 * 1024 * 1000,
timeout: CLAUDE_CODE_TIMEOUT,
@@ -1,43 +0,0 @@
import { getCwd } from "@/utils/path"
import * as vscode from "vscode"
/*
Used to get user's current python environment (unnecessary now that we use the IDE's terminal)
${await (async () => {
try {
const pythonEnvPath = await getPythonEnvPath()
if (pythonEnvPath) {
return `\nPython Environment: ${pythonEnvPath}`
}
} catch {}
return ""
})()}
*/
export async function getPythonEnvPath(): Promise<string | undefined> {
const pythonExtension = vscode.extensions.getExtension("ms-python.python")
if (!pythonExtension) {
return undefined
}
// Ensure the Python extension is activated
if (!pythonExtension.isActive) {
// if the python extension is not active, we can assume the project is not a python project
return undefined
}
// Access the Python extension API
const pythonApi = pythonExtension.exports
// Get the active environment path for the current workspace
const workspaceFolder = await getCwd()
if (!workspaceFolder) {
return undefined
}
// Get the active python environment path for the current workspace
const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(workspaceFolder)
if (pythonEnv && pythonEnv.path) {
return pythonEnv.path
} else {
return undefined
}
}
+1 -6
View File
@@ -4,14 +4,9 @@ import { ExtensionMessage } from "@shared/ExtensionMessage"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
private getClineApiKey: () => Promise<string | undefined>
constructor(
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
getClineApiKey: () => Promise<string | undefined>,
) {
this.postMessageToWebview = postMessageToWebview
constructor(getClineApiKey: () => Promise<string | undefined>) {
this.getClineApiKey = getClineApiKey
}
-1
View File
@@ -9,7 +9,6 @@ import * as path from "path"
import { Logger } from "../logging/Logger"
import { createTestServer, shutdownTestServer } from "./TestServer"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { GetWorkspacePathsRequest } from "@/shared/proto/index.host"
// State variable
let isTestMode = false
+36
View File
@@ -185,6 +185,42 @@ export async function getWorkingState(cwd: string): Promise<string> {
}
}
export async function getGitRemoteUrls(cwd: string): Promise<string[]> {
try {
const isInstalled = await checkGitInstalled()
if (!isInstalled) {
return []
}
const isRepo = await checkGitRepo(cwd)
if (!isRepo) {
return []
}
const { stdout } = await execAsync("git remote -v", { cwd })
if (!stdout.trim()) {
return []
}
// Parse output to extract unique URLs
// git remote -v output format: "remoteName remoteUrl (fetch|push)"
const remotes = stdout
.trim()
.split("\n")
.filter((line) => line.includes("(fetch)")) // Only fetch URLs to avoid duplicates
.map((line) => {
const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/)
return match ? { name: match[1], url: match[2] } : null
})
.filter((remote): remote is { name: string; url: string } => remote !== null)
return remotes.map((remote) => `${remote.name}: ${remote.url}`)
} catch (error) {
console.error("Error getting git remotes:", error)
return []
}
}
function truncateOutput(content: string): string {
if (!GIT_OUTPUT_LINE_LIMIT) {
return content
+8 -2
View File
@@ -1551,7 +1551,10 @@ export const ChatRowContent = memo(
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "followup"}
isActive={
(isLast && lastModifiedMessage?.ask === "followup") ||
(!selected && options && options.length > 0)
}
inputValue={inputValue}
/>
{quoteButtonState.visible && (
@@ -1640,7 +1643,10 @@ export const ChatRowContent = memo(
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_respond"}
isActive={
(isLast && lastModifiedMessage?.ask === "plan_mode_respond") ||
(!selected && options && options.length > 0)
}
inputValue={inputValue}
/>
{quoteButtonState.visible && (
@@ -66,7 +66,7 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
<div style={{ flexGrow: 1, display: "flex" }} ref={scrollContainerRef}>
<Virtuoso
ref={virtuosoRef}
key={task.ts} // trick to make sure virtuoso re-renders when task changes
key={task.ts} // trick to make sure virtuoso re-renders when task changes, and we use initialTopMostItemIndex to start at the bottom
className="scrollable"
style={{
flexGrow: 1,
@@ -75,11 +75,12 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
components={{
Footer: () => <div style={{ height: 5 }} />, // Add empty padding at the bottom
}}
// increasing top by 3_000 to prevent jumping around when user collapses a row
increaseViewportBy={{
top: 3_000,
bottom: Number.MAX_SAFE_INTEGER,
}}
data={groupedMessages}
}} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
data={groupedMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
itemContent={itemContent}
atBottomStateChange={(isAtBottom) => {
setIsAtBottom(isAtBottom)
@@ -88,7 +89,7 @@ export const MessagesArea: React.FC<MessagesAreaProps> = ({
}
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
}}
atBottomThreshold={10}
atBottomThreshold={10} // anything lower causes issues with followOutput
initialTopMostItemIndex={groupedMessages.length - 1}
/>
</div>
@@ -39,55 +39,3 @@ export const useIsStreaming = (
return false
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
}
/**
* Component that shows a visual streaming indicator
* Can be used to show loading states, typing indicators, etc.
*/
export const StreamingVisualIndicator: React.FC<{ isStreaming: boolean }> = ({ isStreaming }) => {
if (!isStreaming) return null
return (
<div
style={{
display: "flex",
alignItems: "center",
padding: "8px 16px",
color: "var(--vscode-descriptionForeground)",
fontSize: "12px",
}}>
<div
style={{
display: "flex",
gap: "4px",
marginRight: "8px",
}}>
{[0, 1, 2].map((i) => (
<div
key={i}
style={{
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "var(--vscode-progressBar-background)",
animation: `pulse 1.4s infinite ease-in-out ${i * 0.16}s`,
}}
/>
))}
</div>
<span>Cline is thinking...</span>
<style>{`
@keyframes pulse {
0%, 80%, 100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1);
}
}
`}</style>
</div>
)
}
@@ -3,4 +3,4 @@
*/
export { MessageRenderer, createMessageRenderer } from "./MessageRenderer"
export { useIsStreaming, StreamingVisualIndicator } from "./StreamingIndicator"
export { useIsStreaming } from "./StreamingIndicator"
@@ -57,6 +57,8 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
case "resume_task":
case "resume_completed_task":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
case "api_req_failed":
case "new_task":
case "condense":
case "report_bug":
@@ -111,34 +113,69 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
// For approval buttons, if there's input content, send it as a proper user message
// If there's no input content, just approve the action
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
// Send as a regular message so it appears in the conversation
await handleSendMessage(trimmedInput || "", images || [], files || [])
} else {
// No input content, just approve the action
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
}),
)
// Clear input state after sending (only when no content was sent as a message)
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
break
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
case "command_output":
// For proceed buttons, if there's input content, send it as a proper user message
// If there's no input content, just proceed with the action
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
text: trimmedInput,
images: images,
files: files,
}),
)
// Send as a regular message so it appears in the conversation
await handleSendMessage(trimmedInput || "", images || [], files || [])
} else {
// No input content, just proceed with the action
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
}),
)
// Clear input state after sending (only when no content was sent as a message)
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
break
case "resume_task":
// For resume_task, if there's input content, send it as a proper user message
// If there's no input content, just resume the task
if (trimmedInput || (images && images.length > 0) || (files && files.length > 0)) {
// Send as a regular message so it appears in the conversation
await handleSendMessage(trimmedInput || "", images || [], files || [])
} else {
// No input content, just resume the task
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
}),
)
// Clear input state after sending (only when no content was sent as a message)
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
// Clear input state after sending
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
break
case "completion_result":
case "resume_completed_task":
@@ -185,6 +222,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
setSendingDisabled,
setEnableButtons,
chatState,
handleSendMessage,
],
)
@@ -32,8 +32,6 @@ export function useScrollBehavior(
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const [isAtBottom, setIsAtBottom] = useState(false)
const [pendingScrollToMessage, setPendingScrollToMessage] = useState<number | null>(null)
// Smooth scroll to bottom with debounce
const scrollToBottomSmooth = useMemo(
() =>
debounce(
@@ -49,7 +47,7 @@ export function useScrollBehavior(
[],
)
// Instant scroll to bottom
// Smooth scroll to bottom with debounce
const scrollToBottomAuto = useCallback(() => {
virtuosoRef.current?.scrollTo({
top: Number.MAX_SAFE_INTEGER,
@@ -57,7 +55,6 @@ export function useScrollBehavior(
})
}, [])
// Scroll to specific message
const scrollToMessage = useCallback(
(messageIndex: number) => {
setPendingScrollToMessage(messageIndex)
@@ -113,7 +110,7 @@ export function useScrollBehavior(
[messages, visibleMessages, groupedMessages],
)
// Toggle row expansion with scroll handling
// scroll when user toggles certain rows
const toggleRowExpansion = useCallback(
(ts: number) => {
const isCollapsing = expandedRows[ts] ?? false
@@ -165,10 +162,9 @@ export function useScrollBehavior(
}
}
},
[groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom, setExpandedRows],
[groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom],
)
// Handle row height changes
const handleRowHeightChange = useCallback(
(isTaller: boolean) => {
if (!disableAutoScrollRef.current) {
@@ -184,23 +180,21 @@ export function useScrollBehavior(
[scrollToBottomSmooth, scrollToBottomAuto],
)
// Auto-scroll when new messages arrive
useEffect(() => {
if (!disableAutoScrollRef.current) {
setTimeout(() => {
scrollToBottomSmooth()
}, 50)
// return () => clearTimeout(timer) // dont cleanup since if visibleMessages.length changes it cancels.
}
}, [groupedMessages.length, scrollToBottomSmooth])
// Handle pending scroll to message
useEffect(() => {
if (pendingScrollToMessage !== null) {
scrollToMessage(pendingScrollToMessage)
}
}, [pendingScrollToMessage, groupedMessages, scrollToMessage])
// Handle wheel events to detect manual scrolling
const handleWheel = useCallback((event: Event) => {
const wheelEvent = event as WheelEvent
if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
@@ -210,8 +204,7 @@ export function useScrollBehavior(
}
}
}, [])
useEvent("wheel", handleWheel, window, { passive: true })
useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
return {
virtuosoRef,
@@ -20,23 +20,23 @@ export function filterVisibleMessages(messages: ClineMessage[]): ClineMessage[]
return messages.filter((message) => {
switch (message.ask) {
case "completion_result":
// don't show a chat row for a completion_result ask without text
// don't show a chat row for a completion_result ask without text. This specific type of message only occurs if cline wants to execute a command as part of its completion result, in which case we interject the completion_result tool with the execute_command tool.
if (message.text === "") {
return false
}
break
case "api_req_failed":
case "api_req_failed": // this message is used to update the latest api_req_started that the request failed
case "resume_task":
case "resume_completed_task":
return false
}
switch (message.say) {
case "api_req_finished":
case "api_req_retried":
case "deleted_api_reqs":
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
case "api_req_retried": // this message is used to update the latest api_req_started that the request was retried
case "deleted_api_reqs": // aggregated api_req metrics from deleted messages
return false
case "text":
// Sometimes cline returns an empty text message, we don't want to render these
// Sometimes cline returns an empty text message, we don't want to render these. (We also use a say text for user messages, so in case they just sent images we still render that)
if ((message.text ?? "") === "" && (message.images?.length ?? 0) === 0) {
return false
}