From 8ff6362b0017c2ad54bc5f15bffffc7893902ac4 Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Fri, 21 Mar 2025 17:30:28 +0530
Subject: [PATCH 01/34] progress 1
---
creatormodetask.md | 50 +++++++++++++++++++++
src/activate/registerCommands.ts | 4 +-
src/core/webview/ClineProvider.ts | 9 ++++
src/extension.ts | 6 +--
webview-ui/src/App.tsx | 7 +--
webview-ui/src/components/chat/ChatView.tsx | 2 +-
webview-ui/src/creator/creator.tsx | 5 +++
webview-ui/src/index.tsx | 6 +++
8 files changed, 80 insertions(+), 9 deletions(-)
create mode 100644 creatormodetask.md
create mode 100644 webview-ui/src/creator/creator.tsx
diff --git a/creatormodetask.md b/creatormodetask.md
new file mode 100644
index 00000000000..0c810e1b393
--- /dev/null
+++ b/creatormodetask.md
@@ -0,0 +1,50 @@
+# Creator Mode Task Description
+
+## Overview
+
+This project is an **agentic AI extension** for VSCode, forked from **RooCode**, which itself is a fork of **Cline**. Our fork is called **PearAI-Roo-Code**.
+
+## Goal
+
+We are implementing a **new view called Creator Mode** into this extension. The objective is to introduce a **separate GUI file structure** to minimize merge conflicts when pulling upstream. However, separation is not a strict requirement—if it makes more sense to integrate changes within the existing structure, we will do so.
+
+## Guidelines
+
+- **Maintain Separation:** GUI files should be kept separate to minimize merge conflicts when pulling upstream.
+- **Flexible Code Organization:** While separation is preferred, prioritize maintainability and efficiency over strict separation.
+- **Keep This Document Updated:** Ensure this file always reflects the latest task status for continuity.
+- **Clear and Concise Development:** No unnecessary complexity—keep solutions to the point and functional.
+
+## Creator Mode Details
+
+- Creator Mode will feature a **text input box** at the center, allowing users to enter prompts.
+- The design will differ from the existing **ChatView** (`chatview.tsx`), but function similarly in terms of input handling.
+- When the user enters a prompt, it will be sent to the **selected AI model**.
+- The response will generate a **new file containing an action plan**, which the user can edit directly.
+- A new **mode identifier** (`Creator Mode`) will be introduced, similar to the existing **Ask Mode** and **Architect Mode**.
+
+## Development Plan (Step-by-Step)
+
+[COMPLETED] - add completed tasks here
+
+[NEXT STEPS]
+
+1. **Initial Input Box Implementation**
+
+ - Create a simple input box that sends user input to the selected model.
+ - Ensure basic communication between the UI and backend.
+ - Validate that the model receives and processes input correctly.
+
+2. **Introduce Creator Mode**
+
+ - Understand how existing "ask" mode and "architect" mode are implemented.
+ - Add `Creator Mode` to the list of available modes.
+ - Ensure the UI adapts accordingly when this mode is selected.
+
+3. **Implement File Edit Functionality**
+ - Modify the response handling to generate an editable file.
+ - Provide a smooth user experience for modifying action plans.
+
+- Proceed step-by-step, ensuring each milestone is functional before progressing, so begin with step **1: Implementing the input box** and verifying AI model communication, once stable, move on to **step 2: introducing Creator Mode**.
+
+This file should be regularly updated to reflect the project's current state and objectives.
diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts
index 69e257e7a51..f93558caf4d 100644
--- a/src/activate/registerCommands.ts
+++ b/src/activate/registerCommands.ts
@@ -51,7 +51,7 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit editor.viewColumn || 0))
@@ -82,5 +82,5 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
+ window.isCreator="${this.isCreator}";
`
@@ -426,6 +433,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort} http://localhost:8000 http://0.0.0.0:8000 https://stingray-app-gb2an.ondigitalocean.app`,
]
+ console.dir("CREATORRRRRR")
+ console.dir(this.isCreator)
return /*html*/ `
diff --git a/src/extension.ts b/src/extension.ts
index 5dcfd3169e5..7ec50ee92f3 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -35,7 +35,7 @@ export function activate(context: vscode.ExtensionContext) {
context.globalState.update("allowedCommands", defaultCommands)
}
- const sidebarProvider = new ClineProvider(context, outputChannel)
+ const sidebarProvider = new ClineProvider(context, outputChannel, false)
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
@@ -111,7 +111,7 @@ export function activate(context: vscode.ExtensionContext) {
outputChannel.appendLine("Opening Roo Code in new tab")
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
- const tabProvider = new ClineProvider(context, outputChannel)
+ const tabProvider = new ClineProvider(context, outputChannel, true)
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
@@ -137,7 +137,7 @@ export function activate(context: vscode.ExtensionContext) {
// Lock the editor group so clicking on files doesn't open them over the panel
await delay(100)
- await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
+ // await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
}
// context.subscriptions.push(vscode.commands.registerCommand("roo-cline.popoutButtonClicked", openClineInNewTab))
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index 740ceb11acc..70fa2efb4d1 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -12,6 +12,7 @@ import WelcomeView from "./components/welcome/WelcomeView"
import McpView from "./components/mcp/McpView"
import PromptsView from "./components/prompts/PromptsView"
import { Inspector } from "react-dev-inspector"
+import Creator from "./creator/creator"
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
@@ -83,10 +84,10 @@ const App = () => {
)
}
+const tempIsCreator: boolean = true
+
const AppWithProviders = () => (
-
-
-
+ {window.isCreator === "true" ? : }
)
export default AppWithProviders
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index cd2773d0808..ccd98738e35 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -1022,7 +1022,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
+
+ >
+ )}
+
+
)
}
From 391b2d5da31723eae482b912d4e9870d2526731d Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Mon, 24 Mar 2025 20:30:19 +0530
Subject: [PATCH 04/34] progress
---
webview-ui/src/creator/chatTextArea.tsx | 0
webview-ui/src/creator/creator.tsx | 1374 +++++++++++++++++++----
2 files changed, 1137 insertions(+), 237 deletions(-)
create mode 100644 webview-ui/src/creator/chatTextArea.tsx
diff --git a/webview-ui/src/creator/chatTextArea.tsx b/webview-ui/src/creator/chatTextArea.tsx
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/webview-ui/src/creator/creator.tsx b/webview-ui/src/creator/creator.tsx
index 0aaaa9621b5..5c23c5e1a21 100644
--- a/webview-ui/src/creator/creator.tsx
+++ b/webview-ui/src/creator/creator.tsx
@@ -1,283 +1,1183 @@
-import { useState, useCallback, useEffect, useRef } from "react"
-import { vscode } from "../utils/vscode"
-import { WebviewMessage } from "../../../src/shared/WebviewMessage"
-import { ExtensionMessage, ClineMessage } from "../../../src/shared/ExtensionMessage"
-import { Markdown } from "../components/ui/markdown"
+import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
+import debounce from "debounce"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { useDeepCompareEffect, useEvent, useMount } from "react-use"
+import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import styled from "styled-components"
-// Styled components for split view
-const Container = styled.div`
- display: flex;
- height: 100vh;
- background: var(--vscode-editor-background);
- color: var(--vscode-foreground);
-`
-
-const ChatPanel = styled.div`
- flex: 1;
- display: flex;
- flex-direction: column;
- padding: 1rem;
- overflow: hidden;
-`
+import splashIcon from "../../../assets/icons/pearai-agent-splash.svg"
+import AutoApproveMenu from "@/components/chat/AutoApproveMenu"
+import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
+import ChatRow from "@/components/chat/ChatRow"
+import ChatTextArea from "@/components/chat/ChatTextArea"
+import TaskHeader from "@/components/chat/TaskHeader"
+import HistoryPreview from "@/components/history/HistoryPreview"
+import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
+import { Button, vscButtonBackground } from "@/components/ui"
+import { useExtensionState } from "@/context/ExtensionStateContext"
+import { usePearAiModels } from "@/hooks/usePearAiModels"
+import { validateCommand } from "@/utils/command-validation"
+import { vscode } from "@/utils/vscode"
+import { findLast } from "../../../src/shared/array"
+import { combineApiRequests } from "../../../src/shared/combineApiRequests"
+import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
+import { ClineAsk, ClineSayTool, ExtensionMessage, ClineMessage, ClineSayBrowserAction } from "../../../src/shared/ExtensionMessage"
+import { getApiMetrics } from "../../../src/shared/getApiMetrics"
+import { McpServer, McpTool } from "../../../src/shared/mcp"
+import { AudioType } from "../../../src/shared/WebviewMessage"
-interface TaskPlanPanelProps {
- visible: boolean
+interface ChatViewProps {
+ isHidden: boolean
+ showAnnouncement: boolean
+ hideAnnouncement: () => void
+ showHistoryView: () => void
}
-const TaskPlanPanel = styled.div`
- width: 40%;
- border-left: 1px solid var(--vscode-sideBar-border);
- padding: 1rem;
- overflow-y: auto;
- background: var(--vscode-editor-background);
- display: ${(props) => (props.visible ? "block" : "none")};
- position: relative;
-
- opacity: ${(props) => (props.visible ? 1 : 0)};
- transition: opacity 0.3s ease-in-out;
-
- ${(props) =>
- props.visible &&
- `
- border: 2px solid var(--vscode-textLink-foreground);
- `}
-`
+export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
-const TaskPlanHeader = styled.div`
- font-size: 1.2rem;
- font-weight: bold;
- margin-bottom: 1rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--vscode-sideBar-border);
-`
+const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
+ const {
+ version,
+ clineMessages: messages,
+ taskHistory,
+ apiConfiguration,
+ mcpServers,
+ alwaysAllowBrowser,
+ alwaysAllowReadOnly,
+ alwaysAllowWrite,
+ alwaysAllowExecute,
+ alwaysAllowMcp,
+ allowedCommands,
+ writeDelayMs,
+ mode,
+ setMode,
+ autoApprovalEnabled,
+ alwaysAllowModeSwitch,
+ } = useExtensionState()
-const MessageContainer = styled.div`
- flex: 1;
- overflow-y: auto;
- margin-bottom: 1rem;
- padding-right: 0.5rem;
-`
+ //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
+ const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
+ const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
+ // has to be after api_req_finished are all reduced into api_req_started messages
+ const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
-const Message = styled.div`
- margin: 0.5rem 0;
- padding: 0.75rem;
- border-radius: 0.375rem;
- max-width: 80%;
+ const [inputValue, setInputValue] = useState("")
+ const textAreaRef = useRef(null)
+ const [textAreaDisabled, setTextAreaDisabled] = useState(false)
+ const [selectedImages, setSelectedImages] = useState([])
- &.user {
- margin-left: auto;
- background: #e64c9e;
- color: white;
- }
+ // 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(undefined)
+ const [enableButtons, setEnableButtons] = useState(false)
+ const [primaryButtonText, setPrimaryButtonText] = useState(undefined)
+ const [secondaryButtonText, setSecondaryButtonText] = useState(undefined)
+ const [didClickCancel, setDidClickCancel] = useState(false)
+ const virtuosoRef = useRef(null)
+ const [expandedRows, setExpandedRows] = useState>({})
+ const scrollContainerRef = useRef(null)
+ const disableAutoScrollRef = useRef(false)
+ const [showScrollToBottom, setShowScrollToBottom] = useState(false)
+ const [isAtBottom, setIsAtBottom] = useState(false)
- &.assistant {
- background: var(--vscode-editor-background);
- border: 1px solid var(--vscode-input-border);
- }
-`
+ const [wasStreaming, setWasStreaming] = useState(false)
-const InputContainer = styled.div`
- display: flex;
- gap: 1rem;
- padding: 1rem;
- background: var(--vscode-editor-background);
- border: 1px solid var(--vscode-input-border);
- border-radius: 0.5rem;
-`
+ // UI layout depends on the last 2 messages
+ // (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
+ const lastMessage = useMemo(() => messages.at(-1), [messages])
+ const secondLastMessage = useMemo(() => messages.at(-2), [messages])
-const StyledTextArea = styled.textarea`
- width: 100%;
- padding: 0.5rem 1rem;
- background: var(--vscode-input-background);
- color: var(--vscode-input-foreground);
- border: none;
- border-radius: 0.5rem;
- resize: none;
-
- &:focus {
- outline: none;
- box-shadow: 0 0 0 2px #e64c9e;
+ function playSound(audioType: AudioType) {
+ vscode.postMessage({ type: "playSound", audioType })
}
-`
-const SendButton = styled.button`
- padding: 0.5rem 1.5rem;
- background: #e64c9e;
- color: white;
- border: none;
- border-radius: 0.5rem;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- transition: all 0.2s;
+ useDeepCompareEffect(() => {
+ // if last message is an ask, show user ask UI
+ // if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
+ // basically as long as a task is active, the conversation history will be persisted
+ if (lastMessage) {
+ switch (lastMessage.type) {
+ case "ask":
+ const isPartial = lastMessage.partial === true
+ switch (lastMessage.ask) {
+ case "api_req_failed":
+ playSound("progress_loop")
+ setTextAreaDisabled(true)
+ setClineAsk("api_req_failed")
+ setEnableButtons(true)
+ setPrimaryButtonText("Retry")
+ setSecondaryButtonText("Start New Task")
+ break
+ case "mistake_limit_reached":
+ playSound("progress_loop")
+ setTextAreaDisabled(false)
+ setClineAsk("mistake_limit_reached")
+ setEnableButtons(true)
+ setPrimaryButtonText("Proceed Anyways")
+ setSecondaryButtonText("Start New Task")
+ break
+ case "followup":
+ setTextAreaDisabled(isPartial)
+ setClineAsk("followup")
+ setEnableButtons(isPartial)
+ // setPrimaryButtonText(undefined)
+ // setSecondaryButtonText(undefined)
+ break
+ case "tool":
+ if (!isAutoApproved(lastMessage)) {
+ playSound("notification")
+ }
+ setTextAreaDisabled(isPartial)
+ setClineAsk("tool")
+ setEnableButtons(!isPartial)
+ const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
+ switch (tool.tool) {
+ case "editedExistingFile":
+ case "appliedDiff":
+ case "newFileCreated":
+ setPrimaryButtonText("Save")
+ setSecondaryButtonText("Reject")
+ break
+ default:
+ setPrimaryButtonText("Approve")
+ setSecondaryButtonText("Reject")
+ break
+ }
+ break
+ case "browser_action_launch":
+ if (!isAutoApproved(lastMessage)) {
+ playSound("notification")
+ }
+ setTextAreaDisabled(isPartial)
+ setClineAsk("browser_action_launch")
+ setEnableButtons(!isPartial)
+ setPrimaryButtonText("Approve")
+ setSecondaryButtonText("Reject")
+ break
+ case "command":
+ if (!isAutoApproved(lastMessage)) {
+ playSound("notification")
+ }
+ setTextAreaDisabled(isPartial)
+ setClineAsk("command")
+ setEnableButtons(!isPartial)
+ setPrimaryButtonText("Run Command")
+ setSecondaryButtonText("Reject")
+ break
+ case "command_output":
+ setTextAreaDisabled(false)
+ setClineAsk("command_output")
+ setEnableButtons(true)
+ setPrimaryButtonText("Proceed While Running")
+ setSecondaryButtonText(undefined)
+ break
+ case "use_mcp_server":
+ setTextAreaDisabled(isPartial)
+ setClineAsk("use_mcp_server")
+ setEnableButtons(!isPartial)
+ setPrimaryButtonText("Approve")
+ setSecondaryButtonText("Reject")
+ break
+ case "completion_result":
+ // extension waiting for feedback. but we can just present a new task button
+ playSound("celebration")
+ setTextAreaDisabled(isPartial)
+ setClineAsk("completion_result")
+ setEnableButtons(!isPartial)
+ setPrimaryButtonText("Start New Task")
+ setSecondaryButtonText(undefined)
+ break
+ case "resume_task":
+ setTextAreaDisabled(false)
+ setClineAsk("resume_task")
+ setEnableButtons(true)
+ setPrimaryButtonText("Resume Task")
+ setSecondaryButtonText("Terminate")
+ setDidClickCancel(false) // special case where we reset the cancel button state
+ break
+ case "resume_completed_task":
+ setTextAreaDisabled(false)
+ setClineAsk("resume_completed_task")
+ setEnableButtons(true)
+ setPrimaryButtonText("Start New Task")
+ setSecondaryButtonText(undefined)
+ setDidClickCancel(false)
+ break
+ }
+ break
+ case "say":
+ // don't want to reset since there could be a "say" after an "ask" while ask is waiting for response
+ switch (lastMessage.say) {
+ case "api_req_retry_delayed":
+ setTextAreaDisabled(true)
+ break
+ case "api_req_started":
+ if (secondLastMessage?.ask === "command_output") {
+ // if the last ask is a command_output, 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 case, the user has to interact with input field or buttons to continue, which does the following automatically)
+ setInputValue("")
+ setTextAreaDisabled(true)
+ setSelectedImages([])
+ setClineAsk(undefined)
+ setEnableButtons(false)
+ }
+ break
+ case "api_req_finished":
+ case "task":
+ 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":
+ case "tool":
+ break
+ }
+ break
+ }
+ } else {
+ // 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
+ // setTextAreaDisabled(false)
+ // setClineAsk(undefined)
+ // setPrimaryButtonText(undefined)
+ // setSecondaryButtonText(undefined)
+ }
+ }, [lastMessage, secondLastMessage])
- &:hover:not(:disabled) {
- background: #d33c8e;
- transform: scale(1.05);
- }
+ useEffect(() => {
+ if (messages.length === 0) {
+ setTextAreaDisabled(false)
+ setClineAsk(undefined)
+ setEnableButtons(false)
+ setPrimaryButtonText(undefined)
+ setSecondaryButtonText(undefined)
+ }
+ }, [messages.length])
- &:active:not(:disabled) {
- transform: scale(0.95);
- }
+ useEffect(() => {
+ setExpandedRows({})
+ }, [task?.ts])
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-`
+ const isStreaming = useMemo(() => {
+ 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 =
+ isLastAsk && clineAsk !== undefined && enableButtons && primaryButtonText !== undefined
+ if (isToolCurrentlyAsking) {
+ return false
+ }
-const Creator = () => {
- const [messages, setMessages] = useState([])
- const [inputValue, setInputValue] = useState("")
- const [isLoading, setIsLoading] = useState(false)
- const [taskPlan, setTaskPlan] = useState(null)
- const messagesEndRef = useRef(null)
+ const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
+ if (isLastMessagePartial) {
+ return true
+ } else {
+ const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
+ if (
+ lastApiReqStarted &&
+ lastApiReqStarted.text !== null &&
+ lastApiReqStarted.text !== undefined &&
+ lastApiReqStarted.say === "api_req_started"
+ ) {
+ const cost = JSON.parse(lastApiReqStarted.text).cost
+ if (cost === undefined) {
+ // api request has not finished yet
+ return true
+ }
+ }
+ }
+
+ return false
+ }, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])
+
+ const handleSendMessage = useCallback(
+ (text: string, images: string[]) => {
+ text = text.trim()
+ if (text || images.length > 0) {
+ if (messages.length === 0) {
+ vscode.postMessage({ type: "newTask", text, images })
+ } else if (clineAsk) {
+ switch (clineAsk) {
+ case "followup":
+ 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":
+ case "resume_completed_task":
+ case "mistake_limit_reached":
+ vscode.postMessage({
+ type: "askResponse",
+ askResponse: "messageResponse",
+ text,
+ images,
+ })
+ break
+ // there is no other case that a textfield should be enabled
+ }
+ }
+ // Only reset message-specific state, preserving mode
+ setInputValue("")
+ setTextAreaDisabled(true)
+ setSelectedImages([])
+ setClineAsk(undefined)
+ setEnableButtons(false)
+ // Do not reset mode here as it should persist
+ // setPrimaryButtonText(undefined)
+ // setSecondaryButtonText(undefined)
+ disableAutoScrollRef.current = false
+ }
+ },
+ [messages.length, clineAsk],
+ )
+
+ const handleSetChatBoxMessage = useCallback(
+ (text: string, images: string[]) => {
+ // Avoid nested template literals by breaking down the logic
+ let newValue = text
+ if (inputValue !== "") {
+ newValue = inputValue + " " + text
+ }
+
+ setInputValue(newValue)
+ setSelectedImages([...selectedImages, ...images])
+ },
+ [inputValue, selectedImages],
+ )
+
+ const startNewTask = useCallback(() => {
+ vscode.postMessage({ type: "clearTask" })
+ }, [])
+
+ /*
+ 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(
+ (text?: string, images?: string[]) => {
+ const trimmedInput = text?.trim()
+ switch (clineAsk) {
+ case "api_req_failed":
+ case "command":
+ case "command_output":
+ case "tool":
+ case "browser_action_launch":
+ case "use_mcp_server":
+ case "resume_task":
+ case "mistake_limit_reached":
+ // Only send text/images if they exist
+ if (trimmedInput || (images && images.length > 0)) {
+ vscode.postMessage({
+ type: "askResponse",
+ askResponse: "yesButtonClicked",
+ text: trimmedInput,
+ images: images,
+ })
+ } else {
+ vscode.postMessage({
+ type: "askResponse",
+ askResponse: "yesButtonClicked",
+ })
+ }
+ // Clear input state after sending
+ setInputValue("")
+ setSelectedImages([])
+ break
+ case "completion_result":
+ case "resume_completed_task":
+ // extension waiting for feedback. but we can just present a new task button
+ startNewTask()
+ break
+ }
+ setTextAreaDisabled(true)
+ setClineAsk(undefined)
+ setEnableButtons(false)
+ disableAutoScrollRef.current = false
+ },
+ [clineAsk, startNewTask],
+ )
+
+ const handleSecondaryButtonClick = useCallback(
+ (text?: string, images?: string[]) => {
+ const trimmedInput = text?.trim()
+ if (isStreaming) {
+ vscode.postMessage({ type: "cancelTask" })
+ setDidClickCancel(true)
+ return
+ }
+
+ switch (clineAsk) {
+ case "api_req_failed":
+ case "mistake_limit_reached":
+ case "resume_task":
+ startNewTask()
+ break
+ case "command":
+ case "tool":
+ case "browser_action_launch":
+ case "use_mcp_server":
+ // Only send text/images if they exist
+ if (trimmedInput || (images && images.length > 0)) {
+ vscode.postMessage({
+ type: "askResponse",
+ askResponse: "noButtonClicked",
+ text: trimmedInput,
+ images: images,
+ })
+ } else {
+ // responds to the API with a "This operation failed" and lets it try again
+ vscode.postMessage({
+ type: "askResponse",
+ askResponse: "noButtonClicked",
+ })
+ }
+ // Clear input state after sending
+ setInputValue("")
+ setSelectedImages([])
+ break
+ }
+ setTextAreaDisabled(true)
+ setClineAsk(undefined)
+ setEnableButtons(false)
+ disableAutoScrollRef.current = false
+ },
+ [clineAsk, startNewTask, isStreaming],
+ )
+
+ const handleTaskCloseButtonClick = useCallback(() => {
+ startNewTask()
+ }, [startNewTask])
+
+ const pearAiModels = usePearAiModels(apiConfiguration)
+
+ const { selectedModelInfo } = useMemo(() => {
+ return normalizeApiConfiguration(apiConfiguration, pearAiModels)
+ }, [apiConfiguration, pearAiModels])
+
+ const selectImages = useCallback(() => {
+ vscode.postMessage({ type: "selectImages" })
+ }, [])
+
+ const shouldDisableImages =
+ !selectedModelInfo.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
+
+ const handleMessage = useCallback(
+ (e: MessageEvent) => {
+ const message: ExtensionMessage = e.data
+ switch (message.type) {
+ case "action":
+ switch (message.action!) {
+ case "didBecomeVisible":
+ if (!isHidden && !textAreaDisabled && !enableButtons) {
+ textAreaRef.current?.focus()
+ }
+ break
+ }
+ break
+ case "selectedImages":
+ const newImages = message.images ?? []
+ if (newImages.length > 0) {
+ setSelectedImages((prevImages) =>
+ [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE),
+ )
+ }
+ break
+ case "invoke":
+ switch (message.invoke!) {
+ case "sendMessage":
+ handleSendMessage(message.text ?? "", message.images ?? [])
+ break
+ case "setChatBoxMessage":
+ handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
+ break
+ case "primaryButtonClick":
+ handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
+ break
+ case "secondaryButtonClick":
+ handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
+ break
+ }
+ }
+ // textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference.
+ },
+ [
+ isHidden,
+ textAreaDisabled,
+ enableButtons,
+ handleSendMessage,
+ handleSetChatBoxMessage,
+ handlePrimaryButtonClick,
+ handleSecondaryButtonClick,
+ ],
+ )
+
+ useEvent("message", handleMessage)
+
+ useMount(() => {
+ // NOTE: the vscode window needs to be focused for this to work
+ textAreaRef.current?.focus()
+ })
- // Add effect to log taskPlan changes
useEffect(() => {
- console.log("[creator] TaskPlan state changed:", taskPlan)
- }, [taskPlan])
+ const timer = setTimeout(() => {
+ if (!isHidden && !textAreaDisabled && !enableButtons) {
+ textAreaRef.current?.focus()
+ }
+ }, 50)
+ return () => {
+ clearTimeout(timer)
+ }
+ }, [isHidden, textAreaDisabled, enableButtons])
- const scrollToBottom = () => {
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
- }
+ const visibleMessages = useMemo(() => {
+ return modifiedMessages.filter((message) => {
+ switch (message.ask) {
+ case "completion_result":
+ // 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": // 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": // 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 "api_req_deleted": // aggregated api_req metrics from deleted messages
+ return false
+ case "api_req_retry_delayed":
+ // Only show the retry message if it's the last message
+ return message === modifiedMessages.at(-1)
+ case "text":
+ // 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
+ }
+ break
+ case "mcp_server_request_started":
+ return false
+ }
+ return true
+ })
+ }, [modifiedMessages])
+
+ const isReadOnlyToolAction = useCallback((message: ClineMessage | undefined) => {
+ if (message?.type === "ask") {
+ if (!message.text) {
+ return true
+ }
+ const tool = JSON.parse(message.text)
+ return [
+ "readFile",
+ "listFiles",
+ "listFilesTopLevel",
+ "listFilesRecursive",
+ "listCodeDefinitionNames",
+ "searchFiles",
+ ].includes(tool.tool)
+ }
+ return false
+ }, [])
+
+ const isWriteToolAction = useCallback((message: ClineMessage | undefined) => {
+ if (message?.type === "ask") {
+ if (!message.text) {
+ return true
+ }
+ const tool = JSON.parse(message.text)
+ return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool)
+ }
+ return false
+ }, [])
+
+ const isMcpToolAlwaysAllowed = useCallback(
+ (message: ClineMessage | undefined) => {
+ if (message?.type === "ask" && message.ask === "use_mcp_server") {
+ if (!message.text) {
+ return true
+ }
+ const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string }
+ if (mcpServerUse.type === "use_mcp_tool") {
+ const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
+ const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
+ return tool?.alwaysAllow || false
+ }
+ }
+ return false
+ },
+ [mcpServers],
+ )
+
+ // Check if a command message is allowed
+ const isAllowedCommand = useCallback(
+ (message: ClineMessage | undefined): boolean => {
+ if (message?.type !== "ask") return false
+ return validateCommand(message.text || "", allowedCommands || [])
+ },
+ [allowedCommands],
+ )
+
+ const isAutoApproved = useCallback(
+ (message: ClineMessage | undefined) => {
+ if (!autoApprovalEnabled || !message || message.type !== "ask") return false
+
+ return (
+ (alwaysAllowBrowser && message.ask === "browser_action_launch") ||
+ (alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
+ (alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
+ (alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
+ (alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) ||
+ (alwaysAllowModeSwitch &&
+ message.ask === "tool" &&
+ (JSON.parse(message.text || "{}")?.tool === "switchMode" ||
+ JSON.parse(message.text || "{}")?.tool === "newTask"))
+ )
+ },
+ [
+ autoApprovalEnabled,
+ alwaysAllowBrowser,
+ alwaysAllowReadOnly,
+ isReadOnlyToolAction,
+ alwaysAllowWrite,
+ isWriteToolAction,
+ alwaysAllowExecute,
+ isAllowedCommand,
+ alwaysAllowMcp,
+ isMcpToolAlwaysAllowed,
+ alwaysAllowModeSwitch,
+ ],
+ )
useEffect(() => {
- scrollToBottom()
- }, [messages])
+ // Only execute when isStreaming changes from true to false
+ if (wasStreaming && !isStreaming && lastMessage) {
+ // Play appropriate sound based on lastMessage content
+ if (lastMessage.type === "ask") {
+ // Don't play sounds for auto-approved actions
+ if (!isAutoApproved(lastMessage)) {
+ switch (lastMessage.ask) {
+ case "api_req_failed":
+ case "mistake_limit_reached":
+ playSound("progress_loop")
+ break
+ case "followup":
+ if (!lastMessage.partial) {
+ playSound("notification")
+ }
+ break
+ case "tool":
+ case "browser_action_launch":
+ case "resume_task":
+ case "use_mcp_server":
+ playSound("notification")
+ break
+ case "completion_result":
+ case "resume_completed_task":
+ playSound("celebration")
+ break
+ }
+ }
+ }
+ }
+ // Update previous value
+ setWasStreaming(isStreaming)
+ }, [isStreaming, lastMessage, wasStreaming, isAutoApproved])
- const handleSendMessage = useCallback(
- (text: string) => {
- if (!text.trim() || isLoading) return
+ const isBrowserSessionMessage = (message: ClineMessage): boolean => {
+ // which of visible messages are browser session messages, see above
+ if (message.type === "ask") {
+ return ["browser_action_launch"].includes(message.ask!)
+ }
+ if (message.type === "say") {
+ return ["api_req_started", "text", "browser_action", "browser_action_result"].includes(message.say!)
+ }
+ return false
+ }
- // Add user message
- setMessages((prev) => [
- ...prev,
- {
- type: "say",
- say: "text",
- text: text.trim(),
- ts: Date.now(),
+ const groupedMessages = useMemo(() => {
+ const result: (ClineMessage | ClineMessage[])[] = []
+ let currentGroup: ClineMessage[] = []
+ let isInBrowserSession = false
+
+ const endBrowserSession = () => {
+ if (currentGroup.length > 0) {
+ result.push([...currentGroup])
+ currentGroup = []
+ isInBrowserSession = false
+ }
+ }
+
+ visibleMessages.forEach((message) => {
+ if (message.ask === "browser_action_launch") {
+ // complete existing browser session if any
+ endBrowserSession()
+ // start new
+ isInBrowserSession = true
+ currentGroup.push(message)
+ } else if (isInBrowserSession) {
+ // end session if api_req_started is cancelled
+
+ if (message.say === "api_req_started") {
+ // get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session
+ const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
+ if (lastApiReqStarted?.text !== null && lastApiReqStarted?.text !== undefined) {
+ const info = JSON.parse(lastApiReqStarted.text)
+ const isCancelled = info.cancelReason !== null && info.cancelReason !== undefined
+ if (isCancelled) {
+ endBrowserSession()
+ result.push(message)
+ return
+ }
+ }
+ }
+
+ if (isBrowserSessionMessage(message)) {
+ currentGroup.push(message)
+
+ // Check if this is a close action
+ if (message.say === "browser_action") {
+ const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
+ if (browserAction.action === "close") {
+ endBrowserSession()
+ }
+ }
+ } else {
+ // complete existing browser session if any
+ endBrowserSession()
+ result.push(message)
+ }
+ } else {
+ result.push(message)
+ }
+ })
+
+ // Handle case where browser session is the last group
+ if (currentGroup.length > 0) {
+ result.push([...currentGroup])
+ }
+
+ return result
+ }, [visibleMessages])
+
+ // scrolling
+
+ const scrollToBottomSmooth = useMemo(
+ () =>
+ debounce(
+ () => {
+ virtuosoRef.current?.scrollTo({
+ top: Number.MAX_SAFE_INTEGER,
+ behavior: "smooth",
+ })
},
- ])
+ 10,
+ { immediate: true },
+ ),
+ [],
+ )
+
+ const scrollToBottomAuto = useCallback(() => {
+ virtuosoRef.current?.scrollTo({
+ top: Number.MAX_SAFE_INTEGER,
+ behavior: "auto", // instant causes crash
+ })
+ }, [])
- setIsLoading(true)
+ // scroll when user toggles certain rows
+ const toggleRowExpansion = useCallback(
+ (ts: number) => {
+ const isCollapsing = expandedRows[ts] ?? false
+ const lastGroup = groupedMessages.at(-1)
+ const isLast = Array.isArray(lastGroup) ? lastGroup[0].ts === ts : lastGroup?.ts === ts
+ const secondToLastGroup = groupedMessages.at(-2)
+ const isSecondToLast = Array.isArray(secondToLastGroup)
+ ? secondToLastGroup[0].ts === ts
+ : secondToLastGroup?.ts === ts
- // Send to extension
- vscode.postMessage({
- type: "newTask",
- text: text.trim(),
- })
+ const isLastCollapsedApiReq =
+ isLast &&
+ !Array.isArray(lastGroup) && // Make sure it's not a browser session group
+ lastGroup?.say === "api_req_started" &&
+ !expandedRows[lastGroup.ts]
- setInputValue("")
+ setExpandedRows((prev) => ({
+ ...prev,
+ [ts]: !prev[ts],
+ }))
+
+ // disable auto scroll when user expands row
+ if (!isCollapsing) {
+ disableAutoScrollRef.current = true
+ }
+
+ if (isCollapsing && isAtBottom) {
+ const timer = setTimeout(() => {
+ scrollToBottomAuto()
+ }, 0)
+ return () => clearTimeout(timer)
+ } else if (isLast || isSecondToLast) {
+ if (isCollapsing) {
+ if (isSecondToLast && !isLastCollapsedApiReq) {
+ return
+ }
+ const timer = setTimeout(() => {
+ scrollToBottomAuto()
+ }, 0)
+ return () => clearTimeout(timer)
+ } else {
+ const timer = setTimeout(() => {
+ virtuosoRef.current?.scrollToIndex({
+ index: groupedMessages.length - (isLast ? 1 : 2),
+ align: "start",
+ })
+ }, 0)
+ return () => clearTimeout(timer)
+ }
+ }
},
- [isLoading],
+ [groupedMessages, expandedRows, scrollToBottomAuto, isAtBottom],
)
- const handleMessage = useCallback((e: MessageEvent) => {
- const message = e.data
-
- console.log("[creator] Received message:", message)
-
- if (message.type === "partialMessage" && message.partialMessage && "type" in message.partialMessage) {
- const partialMessage = message.partialMessage as ClineMessage
- // Only add the message if it's not a duplicate of the last message
- setMessages((prev) => {
- const lastMessage = prev[prev.length - 1]
- if (
- lastMessage &&
- lastMessage.type === partialMessage.type &&
- lastMessage.text === partialMessage.text
- ) {
- return prev
+ const handleRowHeightChange = useCallback(
+ (isTaller: boolean) => {
+ if (!disableAutoScrollRef.current) {
+ if (isTaller) {
+ scrollToBottomSmooth()
+ } else {
+ setTimeout(() => {
+ scrollToBottomAuto()
+ }, 0)
}
- return [...prev, partialMessage]
- })
- setIsLoading(false)
- } else if (message.type === "creator") {
- console.log("[creator] Updating task plan with:", message.text)
- // Only update task plan if it's different from current and not undefined
- const newText = message.text || null
- setTaskPlan((prev) => {
- console.log("[creator] Previous task plan:", prev)
- console.log("[creator] New task plan:", newText)
- return newText !== prev ? newText : prev
- })
+ }
+ },
+ [scrollToBottomSmooth, scrollToBottomAuto],
+ )
+
+ useEffect(() => {
+ if (!disableAutoScrollRef.current) {
+ setTimeout(() => {
+ scrollToBottomSmooth()
+ }, 50)
+ // return () => clearTimeout(timer) // dont cleanup since if visibleMessages.length changes it cancels.
+ }
+ }, [groupedMessages.length, scrollToBottomSmooth])
+
+ const handleWheel = useCallback((event: Event) => {
+ const wheelEvent = event as WheelEvent
+ if (wheelEvent.deltaY && wheelEvent.deltaY < 0) {
+ if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) {
+ // user scrolled up
+ disableAutoScrollRef.current = true
+ }
}
}, [])
+ useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance
+
+ const placeholderText = useMemo(() => {
+ const baseText = task ? "Ask a follow up." : "Give PearAI Agent a task here."
+ const contextText = " Use @ to add context."
+ const imageText = shouldDisableImages ? "" : "\nhold shift to drag in images"
+ const helpText = imageText ? `\n${contextText}${imageText}` : `\n${contextText}`
+ return baseText + contextText
+ }, [task, shouldDisableImages])
+
+ const itemContent = useCallback(
+ (index: number, messageOrGroup: ClineMessage | ClineMessage[]) => {
+ // browser session group
+ if (Array.isArray(messageOrGroup)) {
+ return (
+ expandedRows[messageTs] ?? false}
+ onToggleExpand={(messageTs: number) => {
+ setExpandedRows((prev) => ({
+ ...prev,
+ [messageTs]: !prev[messageTs],
+ }))
+ }}
+ />
+ )
+ }
+
+ // regular message
+ return (
+ toggleRowExpansion(messageOrGroup.ts)}
+ lastModifiedMessage={modifiedMessages.at(-1)}
+ isLast={index === groupedMessages.length - 1}
+ onHeightChange={handleRowHeightChange}
+ isStreaming={isStreaming}
+ />
+ )
+ },
+ [
+ expandedRows,
+ modifiedMessages,
+ groupedMessages.length,
+ handleRowHeightChange,
+ isStreaming,
+ toggleRowExpansion,
+ ],
+ )
useEffect(() => {
- window.addEventListener("message", handleMessage)
- return () => window.removeEventListener("message", handleMessage)
- }, [handleMessage])
+ // Only proceed if we have an ask and buttons are enabled
+ if (!clineAsk || !enableButtons) return
- // Log render with current state
- console.log("[creator] Rendering with taskPlan:", taskPlan)
+ const autoApprove = async () => {
+ if (isAutoApproved(lastMessage)) {
+ // Add delay for write operations
+ if (lastMessage?.ask === "tool" && isWriteToolAction(lastMessage)) {
+ await new Promise((resolve) => setTimeout(resolve, writeDelayMs))
+ }
+ handlePrimaryButtonClick()
+ }
+ }
+ autoApprove()
+ }, [
+ clineAsk,
+ enableButtons,
+ handlePrimaryButtonClick,
+ alwaysAllowBrowser,
+ alwaysAllowReadOnly,
+ alwaysAllowWrite,
+ alwaysAllowExecute,
+ alwaysAllowMcp,
+ messages,
+ allowedCommands,
+ mcpServers,
+ isAutoApproved,
+ lastMessage,
+ writeDelayMs,
+ isWriteToolAction,
+ ])
return (
-
-
-
- {messages.map((msg, i) => (
-
-
-
- ))}
-
- {isLoading && (
-
-
-
-
-
+
+ {task ? (
+
+ ) : (
+
+ {messages.length === 0 && (
+ <>
+
+
+
+
+
+
PearAI Casdfoding Agent
+
+ Powered by Roo Code / Cline
+
+
+
+
+ Autonomous coding agent that has access to your development environment (with
+ your permission) for a feedback loop to add features, fix bugs, and more.
+
-
- >
- )}
-
-
+ {/*
+ // Flex layout explanation:
+ // 1. Content div above uses flex: "1 1 0" to:
+ // - Grow to fill available space (flex-grow: 1)
+ // - Shrink when AutoApproveMenu needs space (flex-shrink: 1)
+ // - Start from zero size (flex-basis: 0) to ensure proper distribution
+ // minHeight: 0 allows it to shrink below its content height
+ //
+ // 2. AutoApproveMenu uses flex: "0 1 auto" to:
+ // - Not grow beyond its content (flex-grow: 0)
+ // - Shrink when viewport is small (flex-shrink: 1)
+ // - Use its content size as basis (flex-basis: auto)
+ // This ensures it takes its natural height when there's space
+ // but becomes scrollable when the viewport is too small
+ */}
+ {!task && (
+
+ )}
+
+ {task && (
+ <>
+
+ , // 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 }} // 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)
+ if (isAtBottom) {
+ disableAutoScrollRef.current = false
+ }
+ setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
+ }}
+ atBottomThreshold={10} // anything lower causes issues with followOutput
+ initialTopMostItemIndex={groupedMessages.length - 1}
+ />
+
+ >
+ )
+ },
+)
+
+export default ChatTextArea
diff --git a/webview-ui/src/creator/creator.tsx b/webview-ui/src/creator/creator.tsx
index 5c23c5e1a21..14da184df38 100644
--- a/webview-ui/src/creator/creator.tsx
+++ b/webview-ui/src/creator/creator.tsx
@@ -9,11 +9,11 @@ import splashIcon from "../../../assets/icons/pearai-agent-splash.svg"
import AutoApproveMenu from "@/components/chat/AutoApproveMenu"
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
import ChatRow from "@/components/chat/ChatRow"
-import ChatTextArea from "@/components/chat/ChatTextArea"
+import ChatTextArea from "./ChatTextArea"
import TaskHeader from "@/components/chat/TaskHeader"
import HistoryPreview from "@/components/history/HistoryPreview"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
-import { Button, vscButtonBackground } from "@/components/ui"
+import { Button, vscBackground, vscButtonBackground, vscEditorBackground, vscSidebarBorder } from "@/components/ui"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { usePearAiModels } from "@/hooks/usePearAiModels"
import { validateCommand } from "@/utils/command-validation"
@@ -975,8 +975,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
display: isHidden ? "none" : "flex",
flexDirection: "column",
overflow: "hidden",
+ backgroundColor: vscBackground,
}}>
- {task ? (
+ {task && (
- ) : (
-
- {messages.length === 0 && (
- <>
-
-
-
-
-
-
PearAI Casdfoding Agent
-
- Powered by Roo Code / Cline
-
-
-
-
- Autonomous coding agent that has access to your development environment (with
- your permission) for a feedback loop to add features, fix bugs, and more.
-
-
-
- >
- )}
- {taskHistory.length > 0 && }
-
)}
{/*
From 4e3787f14a5502860d075b7428eadebea1d4d382 Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Tue, 25 Mar 2025 18:53:09 +0530
Subject: [PATCH 06/34] progress - nice
---
webview-ui/src/creator/ChatRow.tsx | 1121 ++++++++++++++++++++++
webview-ui/src/creator/CodeAccordian.tsx | 127 +++
webview-ui/src/creator/CodeBlock.tsx | 159 +++
webview-ui/src/creator/TaskHeader.tsx | 282 ++++++
webview-ui/src/creator/chatTextArea.tsx | 28 +-
webview-ui/src/creator/creator.tsx | 293 +++---
webview-ui/src/index.css | 4 +
7 files changed, 1858 insertions(+), 156 deletions(-)
create mode 100644 webview-ui/src/creator/ChatRow.tsx
create mode 100644 webview-ui/src/creator/CodeAccordian.tsx
create mode 100644 webview-ui/src/creator/CodeBlock.tsx
create mode 100644 webview-ui/src/creator/TaskHeader.tsx
diff --git a/webview-ui/src/creator/ChatRow.tsx b/webview-ui/src/creator/ChatRow.tsx
new file mode 100644
index 00000000000..2ea5d4a770e
--- /dev/null
+++ b/webview-ui/src/creator/ChatRow.tsx
@@ -0,0 +1,1121 @@
+import { CheckpointSaved } from "@/components/chat/checkpoints/CheckpointSaved"
+import ReasoningBlock from "@/components/chat/ReasoningBlock"
+import { highlightMentions } from "@/components/chat/TaskHeader"
+import { removeLeadingNonAlphanumeric } from "@/components/common/CodeAccordian"
+import CodeAccordian from "./CodeAccordian"
+import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
+import MarkdownBlock from "@/components/common/MarkdownBlock"
+import Thumbnails from "@/components/common/Thumbnails"
+import McpResourceRow from "@/components/mcp/McpResourceRow"
+import McpToolRow from "@/components/mcp/McpToolRow"
+import { vscEditorBackground } from "@/components/ui"
+import { Tail2 } from "@/components/ui/tail"
+import { useExtensionState } from "@/context/ExtensionStateContext"
+import { findMatchingResourceOrTemplate } from "@/utils/mcp"
+import { vscode } from "@/utils/vscode"
+import { ViewfinderCircleIcon } from "@heroicons/react/24/outline"
+import { OpenInNewWindowIcon } from "@radix-ui/react-icons"
+import { VSCodeProgressRing, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
+import { PencilIcon, ServerIcon, PlusCircleIcon } from "lucide-react"
+import React, { memo, useRef, useEffect, useState, useMemo } from "react"
+import { useSize } from "react-use"
+import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
+import { ClineMessage, ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "../../../src/shared/ExtensionMessage"
+import { useCopyToClipboard } from "@/utils/clipboard"
+import deepEqual from "fast-deep-equal"
+
+
+interface ChatRowProps {
+ message: ClineMessage
+ isExpanded: boolean
+ onToggleExpand: () => void
+ lastModifiedMessage?: ClineMessage
+ isLast: boolean
+ onHeightChange: (isTaller: boolean) => void
+ isStreaming: boolean
+}
+
+interface ChatRowContentProps extends Omit {}
+
+const ChatRow = memo(
+ (props: ChatRowProps) => {
+ const { isLast, onHeightChange, message } = props
+ // Store the previous height to compare with the current height
+ // This allows us to detect changes without causing re-renders
+ const prevHeightRef = useRef(0)
+
+ const [chatrow, { height }] = useSize(
+
+
+
,
+ )
+
+ useEffect(() => {
+ // used for partials, command output, etc.
+ // NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
+ const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
+ // height starts off at Infinity
+ if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
+ if (!isInitialRender) {
+ onHeightChange(height > prevHeightRef.current)
+ }
+ prevHeightRef.current = height
+ }
+ }, [height, isLast, onHeightChange, message])
+
+ // we cannot return null as virtuoso does not support it, so we use a separate visibleMessages array to filter out messages that should not be rendered
+ return chatrow
+ },
+ // memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change
+ deepEqual,
+)
+
+export default ChatRow
+
+export const ChatRowContent = ({
+ message,
+ isExpanded,
+ onToggleExpand,
+ lastModifiedMessage,
+ isLast,
+ isStreaming,
+}: ChatRowContentProps) => {
+ const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
+ const [reasoningCollapsed, setReasoningCollapsed] = useState(false)
+
+ // Auto-collapse reasoning when new messages arrive
+ useEffect(() => {
+ if (!isLast && message.say === "reasoning") {
+ setReasoningCollapsed(true)
+ }
+ }, [isLast, message.say])
+ const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
+ if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
+ const info: ClineApiReqInfo = JSON.parse(message.text)
+ return [info.cost, info.cancelReason, info.streamingFailedMessage]
+ }
+ return [undefined, undefined, undefined]
+ }, [message.text, message.say])
+ // when resuming task, last wont be api_req_failed but a resume_task message, so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
+ const apiRequestFailedMessage =
+ isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
+ ? lastModifiedMessage?.text
+ : undefined
+ const isCommandExecuting =
+ isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
+
+ const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started"
+
+ const type = message.type === "ask" ? message.ask : message.say
+
+ const normalColor = "var(--vscode-foreground)"
+ const errorColor = "var(--vscode-errorForeground)"
+ const successColor = "var(--vscode-charts-green)"
+ const cancelledColor = "var(--vscode-descriptionForeground)"
+
+ const [icon, title] = useMemo(() => {
+ switch (type) {
+ case "error":
+ return [
+ ,
+ Error,
+ ]
+ case "mistake_limit_reached":
+ return [
+ ,
+ Roo is having trouble...,
+ ]
+ case "command":
+ return [
+ isCommandExecuting ? (
+
+ ) : (
+
+ ),
+
+ Agent wants to execute this command:
+ ,
+ ]
+ case "use_mcp_server":
+ const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
+ return [
+ isMcpServerResponding ? (
+
+ ) : (
+
+ ),
+
+ Agent wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on
+ the {mcpServerUse.serverName} MCP server:
+ ,
+ ]
+ case "completion_result":
+ return [
+ //
+ // ,
+
+ {toolIcon("folder-opened")}
+
+ {message.type === "ask"
+ ? "Agent wants to view the top level files in this directory:"
+ : "Roo viewed the top level files in this directory:"}
+
+
+ {toolIcon("folder-opened")}
+
+ {message.type === "ask"
+ ? "Agent wants to recursively view all files in this directory:"
+ : "Roo recursively viewed all files in this directory:"}
+
+
+ {toolIcon("file-code")}
+
+ {message.type === "ask"
+ ? "Agent wants to view source code definition names used in this directory:"
+ : "Roo viewed source code definition names used in this directory:"}
+
+
+
+ >
+ )
+ case "searchFiles":
+ return (
+ <>
+
+ {toolIcon("search")}
+
+ {message.type === "ask" ? (
+ <>
+ Agent wants to search this directory for {tool.regex}:
+ >
+ ) : (
+ <>
+ Roo searched this directory for {tool.regex}:
+ >
+ )}
+
+
+ Roo won't be able to view the command's output. Please update VSCode (
+ CMD/CTRL + Shift + P → "Update") and make sure you're using a supported
+ shell: zsh, bash, fish, or PowerShell (CMD/CTRL + Shift + P →
+ "Terminal: Select Default Profile").{" "}
+
+ Still having trouble?
+
+
{
- const baseText = task ? "Ask a follow up." : "Give PearAI Agent a task here."
+ const baseText = task ? "Ask a follow up." : "What would you like do?"
const contextText = " Use @ to add context."
const imageText = shouldDisableImages ? "" : "\nhold shift to drag in images"
const helpText = imageText ? `\n${contextText}${imageText}` : `\n${contextText}`
@@ -976,22 +982,38 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flexDirection: "column",
overflow: "hidden",
backgroundColor: vscBackground,
- }}>
- {task && (
-
- )}
+ }}
+ >
+
+ {task && (
+
+ )}
+
+ {/* */}
- {/*
+ {/*
// Flex layout explanation:
// 1. Content div above uses flex: "1 1 0" to:
// - Grow to fill available space (flex-grow: 1)
@@ -1006,124 +1028,125 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// This ensures it takes its natural height when there's space
// but becomes scrollable when the viewport is too small
*/}
- {!task && (
-
+ )}
+
+ {task && (
+ <>
+
+ , // 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 }} // 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)
+ if (isAtBottom) {
+ disableAutoScrollRef.current = false
+ }
+ setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
+ }}
+ atBottomThreshold={10} // anything lower causes issues with followOutput
+ initialTopMostItemIndex={groupedMessages.length - 1}
+ />
+
- , // 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 }} // 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)
- if (isAtBottom) {
- disableAutoScrollRef.current = false
- }
- setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
- }}
- atBottomThreshold={10} // anything lower causes issues with followOutput
- initialTopMostItemIndex={groupedMessages.length - 1}
- />
-
)
}
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css
index 53025be01a6..e7779acbc7b 100644
--- a/webview-ui/src/index.css
+++ b/webview-ui/src/index.css
@@ -13,6 +13,10 @@
* Reference: https://tailwindcss.com/docs/preflight
*/
+* {
+ /* border: 1px solid red; */
+}
+
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
From 48388f0e6e348f1683f07f508a186c79559e7a41 Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Wed, 26 Mar 2025 16:53:10 +0530
Subject: [PATCH 07/34] progress
---
src/shared/modes.ts | 2 +-
webview-ui/src/creator/chatTextArea.tsx | 4 ++--
webview-ui/src/creator/creator.tsx | 26 ++++++++++++++++++-------
3 files changed, 22 insertions(+), 10 deletions(-)
diff --git a/src/shared/modes.ts b/src/shared/modes.ts
index 968ac84d65e..b51d5a1dd44 100644
--- a/src/shared/modes.ts
+++ b/src/shared/modes.ts
@@ -81,7 +81,7 @@ export const modes: readonly ModeConfig[] = [
"You are PearAI Agent (Powered by Roo Code / Cline), a creative and systematic software architect focused on turning high-level ideas into actionable plans. Your primary goal is to help users transform their ideas into structured action plans.",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
customInstructions:
- "Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Focus on breaking down complex tasks into manageable steps, considering technical requirements, potential challenges, and best practices. The plan should be clear enough that it can be directly implemented by switching to Code mode afterward. (Directly write the plan to a markdown file instead of showing it as normal response.)\n\nThen you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the show_task_plan tool with markdown filename to open the plan markdown file on user's ui for user to see.",
+ "Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Focus on breaking down complex tasks into manageable steps, considering technical requirements, potential challenges, and best practices. The plan should be clear enough that it can be directly implemented by switching to Code mode afterward. (Directly write the plan to a markdown file instead of showing it as normal response.)\n\nThen you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. You only make plans and you should not ask or switch to any other mode.",
},
{
slug: "code",
diff --git a/webview-ui/src/creator/chatTextArea.tsx b/webview-ui/src/creator/chatTextArea.tsx
index 71c8a80255e..e0447e6a840 100644
--- a/webview-ui/src/creator/chatTextArea.tsx
+++ b/webview-ui/src/creator/chatTextArea.tsx
@@ -974,7 +974,7 @@ const ChatTextArea = forwardRef(
}
-
+ )}
+ {isEditing ? (
+
+
+ );
+};
+
+export default SplitView;
\ No newline at end of file
diff --git a/webview-ui/src/creator/creator.tsx b/webview-ui/src/creator/creator.tsx
index 99418bc9eea..0105f721297 100644
--- a/webview-ui/src/creator/creator.tsx
+++ b/webview-ui/src/creator/creator.tsx
@@ -31,6 +31,7 @@ import {
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
import { McpServer, McpTool } from "../../../src/shared/mcp"
import { AudioType } from "../../../src/shared/WebviewMessage"
+import SplitView from './SplitView'
interface ChatViewProps {
isHidden: boolean
@@ -86,12 +87,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [isAtBottom, setIsAtBottom] = useState(false)
const [wasStreaming, setWasStreaming] = useState(false)
+ const [editingFilePath, setEditingFilePath] = useState(null)
// UI layout depends on the last 2 messages
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
const lastMessage = useMemo(() => messages.at(-1), [messages])
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
+ const handleEditPlan = (path: string) => {
+ setEditingFilePath(path)
+ }
+
function playSound(audioType: AudioType) {
vscode.postMessage({ type: "playSound", audioType })
}
@@ -924,6 +930,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
isLast={index === groupedMessages.length - 1}
onHeightChange={handleRowHeightChange}
isStreaming={isStreaming}
+ onEditPlan={handleEditPlan}
/>
)
},
@@ -934,6 +941,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
handleRowHeightChange,
isStreaming,
toggleRowExpansion,
+ handleEditPlan,
],
)
@@ -988,7 +996,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
position: "fixed",
top: 0,
left: 0,
- right: 0,
+ right: editingFilePath ? '50%' : 0,
bottom: 0,
padding: "12px 12px",
display: isHidden ? "none" : "flex",
@@ -1018,7 +1026,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
/>
)}
- {/* */}
+
{/*
// Flex layout explanation:
@@ -1053,19 +1061,51 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
className="borderr rounded-xl">
, // Add empty padding at the bottom
+ Footer: () => ,
+ }}
+ increaseViewportBy={{ top: 3_000, bottom: Number.MAX_SAFE_INTEGER }}
+ data={groupedMessages}
+ itemContent={(index, messageOrGroup) => {
+ if (Array.isArray(messageOrGroup)) {
+ return (
+ expandedRows[messageTs] ?? false}
+ onToggleExpand={(messageTs: number) => {
+ setExpandedRows((prev) => ({
+ ...prev,
+ [messageTs]: !prev[messageTs],
+ }))
+ }}
+ />
+ )
+ }
+
+ return (
+ toggleRowExpansion(messageOrGroup.ts)}
+ lastModifiedMessage={modifiedMessages.at(-1)}
+ isLast={index === groupedMessages.length - 1}
+ onHeightChange={handleRowHeightChange}
+ isStreaming={isStreaming}
+ onEditPlan={handleEditPlan}
+ />
+ )
}}
- // increasing top by 3_000 to prevent jumping around when user collapses a row
- increaseViewportBy={{ top: 3_000, bottom: Number.MAX_SAFE_INTEGER }} // 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)
if (isAtBottom) {
@@ -1073,7 +1113,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom)
}}
- atBottomThreshold={10} // anything lower causes issues with followOutput
+ atBottomThreshold={10}
initialTopMostItemIndex={groupedMessages.length - 1}
/>
@@ -1159,6 +1199,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
isNewTask={taskHistory.length === 0}
/>
+ {editingFilePath && (
+ setEditingFilePath(null)}
+ />
+ )}
)
}
diff --git a/webview-ui/src/utils/getLanguageFromPath.ts b/webview-ui/src/utils/getLanguageFromPath.ts
index 78e8f751daa..a2e0b7befd9 100644
--- a/webview-ui/src/utils/getLanguageFromPath.ts
+++ b/webview-ui/src/utils/getLanguageFromPath.ts
@@ -83,7 +83,61 @@ const extensionToLanguage: { [key: string]: string } = {
// Example usage:
// console.log(getLanguageFromPath('/path/to/file.js')); // Output: javascript
-export function getLanguageFromPath(path: string): string | undefined {
- const extension = path.split(".").pop()?.toLowerCase() || ""
- return extensionToLanguage[extension]
+export function getLanguageFromPath(filePath: string): string {
+ const extension = filePath.split('.').pop()?.toLowerCase() || '';
+
+ // Map file extensions to languages
+ const languageMap: { [key: string]: string } = {
+ 'js': 'javascript',
+ 'jsx': 'javascript',
+ 'ts': 'typescript',
+ 'tsx': 'typescript',
+ 'py': 'python',
+ 'java': 'java',
+ 'cpp': 'cpp',
+ 'c': 'c',
+ 'cs': 'csharp',
+ 'go': 'go',
+ 'rs': 'rust',
+ 'rb': 'ruby',
+ 'php': 'php',
+ 'swift': 'swift',
+ 'kt': 'kotlin',
+ 'scala': 'scala',
+ 'md': 'markdown',
+ 'json': 'json',
+ 'yaml': 'yaml',
+ 'yml': 'yaml',
+ 'xml': 'xml',
+ 'html': 'html',
+ 'css': 'css',
+ 'scss': 'scss',
+ 'sql': 'sql',
+ 'sh': 'shell',
+ 'bash': 'shell',
+ 'zsh': 'shell',
+ 'dockerfile': 'dockerfile',
+ 'vue': 'vue',
+ 'svelte': 'svelte',
+ 'graphql': 'graphql',
+ 'proto': 'protobuf'
+ };
+
+ return languageMap[extension] || extension || 'plaintext';
+}
+
+export function normalizePath(path: string): string {
+ // Convert Windows backslashes to forward slashes
+ let normalized = path.replace(/\\/g, '/');
+
+ // Remove any drive letter prefix (e.g., C:)
+ normalized = normalized.replace(/^[A-Za-z]:/, '');
+
+ // Remove any leading slashes
+ normalized = normalized.replace(/^\/+/, '');
+
+ // Remove any double slashes
+ normalized = normalized.replace(/\/+/g, '/');
+
+ return normalized;
}
From 89e67dbb523b56e85e3330959910bad53fc84203 Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Thu, 27 Mar 2025 10:32:23 +0530
Subject: [PATCH 10/34] progress promtps
---
src/core/prompts/tools/index.ts | 2 --
src/shared/experiments.ts | 2 +-
src/shared/modes.ts | 56 +++++++++++++++++++++++++++++++--
3 files changed, 54 insertions(+), 6 deletions(-)
diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts
index e1bbec168c8..1b9b9a43d9d 100644
--- a/src/core/prompts/tools/index.ts
+++ b/src/core/prompts/tools/index.ts
@@ -13,7 +13,6 @@ import { getUseMcpToolDescription } from "./use-mcp-tool"
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
import { getSwitchModeDescription } from "./switch-mode"
import { getNewTaskDescription } from "./new-task"
-import { getShowTaskPlanDescription } from "./show-task-plan"
import { DiffStrategy } from "../../diff/DiffStrategy"
import { McpHub } from "../../../services/mcp/McpHub"
import { Mode, ModeConfig, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
@@ -110,5 +109,4 @@ export {
getSwitchModeDescription,
getInsertContentDescription,
getSearchAndReplaceDescription,
- getShowTaskPlanDescription,
}
diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts
index 2f946283c0d..5f0a4ce1c35 100644
--- a/src/shared/experiments.ts
+++ b/src/shared/experiments.ts
@@ -40,7 +40,7 @@ export const experimentConfigsMap: Record = {
name: 'Use experimental "power steering" mode',
description:
"When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.",
- enabled: false,
+ enabled: true,
},
}
diff --git a/src/shared/modes.ts b/src/shared/modes.ts
index 9a9aa60f0ee..b681cfbad01 100644
--- a/src/shared/modes.ts
+++ b/src/shared/modes.ts
@@ -72,6 +72,57 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
return Array.from(tools)
}
+
+const CreatorModePrompt = `
+Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Focus on breaking down complex tasks into manageable steps, considering technical requirements, potential challenges, and best practices. The plan should be clear enough that it can be directly implemented by switching to Code mode afterward. (Directly write the plan to a markdown file instead of showing it as normal response.)\n\nOnce you create and write the plan, you mark the task as completed. You only make plans and you should not ask or switch to any other mode.
+
+Best practices:
+For any webapp project, ALWAYS adhere to these rules, unless otherwise specified:
+
+React with TypeScript for the frontend
+Tailwind CSS for styling
+Shadcn/UI components (via Radix UI)
+Supabase for backend/database
+React Query for data management
+React Router for navigation
+Recharts for data visualization
+React Hook Form for form handling
+
+For any other projects always use modern best practices, frameworks, and technologies for the project.
+
+Always use supabase for database or authentication management, unless otherwise specified.
+
+TOOL USE RULES:
+
+Templating:
+Whenever the user requests a new project, follow these steps:
+1. Call the 'list_templates' tool to see available templates.
+2. Match the user request to the closest template.
+3. If a relevant template is found, use the 'create_new_project' tool to create the new project locally
+4. Navigate into the new project directory.
+5. Make all necessary changes to the project files. In this stage, do not implement any database or API integrations, and just focus on the UI. Let the user know about this, and that they can ask to implement these later.
+6. If it is a webapp, use the 'deploy_webapp_from_path' tool to deploy the webapp to the server.
+7. Ask the user to visit the webapp and ask for any feedback.
+8. Confirm success, do not use any computer or browser features to confirm success.
+If no template is relevant, try to fetch a generic template based on the use case. If there are no templates available, do not fetch a template.
+9. Tell the user this is just the first iteration, and they can use PearAI to add any functionality or features to it.
+`
+
+const PearAICodeModePrompt = `
+Deployment:
+If the user is making a webapp and the webapp is completed for review, use the 'deploy_webapp_from_path' tool to deploy the webapp to the server. You will need to upload a zip of the distribution folder, so create one. W
+
+Whenever the user requests a tool, follow these steps:
+1. Call the 'list_tools' tool to see available tools.
+2. Match the user request to the closest tool.
+3. If a relevant tool is found, use the tool to perform the task.
+4. Confirm success.
+
+If no tool is relevant or the user wants a custom approach, do not fetch a tool.
+
+Only proceed if you are over 90 percent confident that you understand what the user wants. If not, ask for clarification.
+`
+
// Main modes configuration as an ordered array
export const modes: readonly ModeConfig[] = [
{
@@ -80,14 +131,13 @@ export const modes: readonly ModeConfig[] = [
roleDefinition:
"You are PearAI Agent (Powered by Roo Code / Cline), a creative and systematic software architect focused on turning high-level ideas into actionable plans. Your primary goal is to help users transform their ideas into structured action plans.",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
- customInstructions:
- "Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Focus on breaking down complex tasks into manageable steps, considering technical requirements, potential challenges, and best practices. The plan should be clear enough that it can be directly implemented by switching to Code mode afterward. (Directly write the plan to a markdown file instead of showing it as normal response.)\n\nOnce you create and write the plan, you mark the task as completed. You only make plans and you should not ask or switch to any other mode.",
+ customInstructions: CreatorModePrompt
},
{
slug: "code",
name: "Code",
roleDefinition:
- "You are PearAI Agent (Powered by Roo Code / Cline), a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
+ "You are PearAI Agent (Powered by Roo Code / Cline), a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices." + PearAICodeModePrompt,
groups: ["read", "edit", "browser", "command", "mcp"],
},
{
From bc610032b98385858df4c470c4e78146c22558ca Mon Sep 17 00:00:00 2001
From: Himanshu
Date: Fri, 28 Mar 2025 17:36:42 +0530
Subject: [PATCH 11/34] editing plan works
---
webview-ui/src/creator/SplitView.tsx | 398 ++++++++++++++-------------
1 file changed, 201 insertions(+), 197 deletions(-)
diff --git a/webview-ui/src/creator/SplitView.tsx b/webview-ui/src/creator/SplitView.tsx
index f9ca3b7400e..8a338124552 100644
--- a/webview-ui/src/creator/SplitView.tsx
+++ b/webview-ui/src/creator/SplitView.tsx
@@ -1,215 +1,219 @@
-import React, { useEffect, useState, useCallback } from 'react';
-import styled from 'styled-components';
-import { vscEditorBackground, vscButtonBackground } from "@/components/ui";
-import { vscode } from "@/utils/vscode";
-import CodeBlock from './CodeBlock';
-import { getLanguageFromPath, normalizePath } from '@/utils/getLanguageFromPath';
-import { WebviewMessage } from '../../../src/shared/WebviewMessage';
+import React, { useEffect, useState, useCallback } from "react"
+import styled from "styled-components"
+import { vscEditorBackground, vscButtonBackground } from "@/components/ui"
+import { vscode } from "@/utils/vscode"
+import CodeBlock from "./CodeBlock"
+import { getLanguageFromPath, normalizePath } from "@/utils/getLanguageFromPath"
+import { WebviewMessage } from "../../../src/shared/WebviewMessage"
interface SplitViewProps {
- filePath: string; // This is workspace-relative path
- onClose: () => void;
+ filePath: string // This is workspace-relative path
+ onClose: () => void
}
interface FileContentMessage {
- type: 'fileContent' | 'error';
- content?: string;
- error?: string;
+ type: "fileContent" | "error"
+ content?: string
+ error?: string
}
const SplitViewContainer = styled.div`
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- width: 50%;
- background-color: ${vscEditorBackground};
- border-left: 1px solid var(--vscode-editorGroup-border);
- display: flex;
- flex-direction: column;
- z-index: 1000;
-`;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 50%;
+ background-color: ${vscEditorBackground};
+ border-left: 1px solid var(--vscode-editorGroup-border);
+ display: flex;
+ flex-direction: column;
+ z-index: 1000;
+`
const Header = styled.div`
- display: flex;
- align-items: center;
- padding: 12px;
- border-bottom: 1px solid var(--vscode-editorGroup-border);
-`;
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ border-bottom: 1px solid var(--vscode-editorGroup-border);
+`
const Title = styled.div`
- flex-grow: 1;
- font-weight: bold;
- margin-right: 12px;
-`;
+ flex-grow: 1;
+ font-weight: bold;
+ margin-right: 12px;
+`
const Content = styled.div`
- flex-grow: 1;
- overflow: auto;
- padding: 12px;
-`;
+ flex-grow: 1;
+ overflow: auto;
+ padding: 12px;
+`
const StyledButton = styled.button`
- background-color: ${vscButtonBackground};
- color: var(--vscode-button-foreground);
- border: none;
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- margin-left: 8px;
-
- &:hover {
- opacity: 0.9;
- }
-`;
+ background-color: ${vscButtonBackground};
+ color: var(--vscode-button-foreground);
+ border: none;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-left: 8px;
+
+ &:hover {
+ opacity: 0.9;
+ }
+`
const SplitView: React.FC = ({ filePath, onClose }) => {
- const [content, setContent] = useState('');
- const [isEditing, setIsEditing] = useState(false);
- const [editedContent, setEditedContent] = useState('');
- const [error, setError] = useState(null);
-
- // Ensure the path has the correct directory structure for display
- const normalizedPath = normalizePath(filePath);
-
- const handleMessage = useCallback((event: MessageEvent) => {
- const message = event.data;
- console.log("SplitView received message:", message);
-
- if (message.type === 'fileContent') {
- if (message.content !== undefined) {
- console.log("Setting content:", message.content);
- setContent(message.content);
- setEditedContent(message.content);
- setError(null);
- // If we were in edit mode, this means a save operation completed successfully
- if (isEditing) {
- console.log("Save operation completed successfully");
- setIsEditing(false);
- } else {
- console.log("Initial file load completed successfully");
- }
- }
- } else if (message.type === 'error') {
- console.log("Received error:", message.error);
- if (!isEditing) {
- // Only clear content if this was a read operation
- console.log("Read operation failed, clearing content");
- setContent('');
- setEditedContent('');
- setIsEditing(true);
- } else {
- console.log("Save operation failed");
- }
- setError(message.error || 'Operation failed');
- }
- }, [isEditing]);
-
- // Log when the component mounts and unmounts
- useEffect(() => {
- console.log("SplitView mounted with filepath:", filePath);
- return () => {
- console.log("SplitView unmounted");
- };
- }, [filePath]);
-
- useEffect(() => {
- console.log("Setting up message listener and initiating file read");
- window.addEventListener('message', handleMessage);
-
- // Read the file
- console.log("Sending readWorkspaceFile message for path:", filePath);
- try {
- vscode.postMessage({
- type: "readWorkspaceFile",
- values: {
- relativePath: filePath,
- create: true,
- ensureDirectory: true,
- content: ''
- }
- });
- console.log("Successfully sent readWorkspaceFile message");
- } catch (error) {
- console.error("Error sending readWorkspaceFile message:", error);
- setError("Failed to send file read request");
- }
-
- // Cleanup
- return () => {
- console.log("Cleaning up message listener");
- window.removeEventListener('message', handleMessage);
- };
- }, [filePath, handleMessage]);
-
- const handleSave = useCallback(() => {
- console.log("Initiating save for file:", filePath);
- console.log("Content length:", editedContent.length);
-
- try {
- // Don't exit edit mode until we get confirmation
- const message: WebviewMessage = {
- type: "writeWorkspaceFile",
- values: {
- relativePath: filePath,
- create: true,
- ensureDirectory: true,
- content: editedContent
- }
- };
- vscode.postMessage(message);
- console.log("Successfully sent writeWorkspaceFile message");
- } catch (error) {
- console.error("Error sending writeWorkspaceFile message:", error);
- setError("Failed to send file save request");
- }
- }, [filePath, editedContent]);
-
- const language = getLanguageFromPath(normalizedPath);
-
- return (
-
-
- {normalizedPath}
- {isEditing ? (
- <>
- Save
- setIsEditing(false)}>Cancel
- >
- ) : (
- setIsEditing(true)}>Edit
- )}
- Close
-
-
- {error && !isEditing && (
-
- {error}
-
- )}
- {isEditing ? (
-
-
- );
-};
-
-export default SplitView;
\ No newline at end of file
+ const [content, setContent] = useState("")
+ const [isEditing, setIsEditing] = useState(false)
+ const [editedContent, setEditedContent] = useState("")
+ const [error, setError] = useState(null)
+
+ // Ensure the path has the correct directory structure for display
+ const normalizedPath = normalizePath(filePath)
+
+ const handleMessage = useCallback((event: MessageEvent) => {
+ const message = event.data
+ console.log("SplitView received message:", message)
+
+ if (message.type === "fileContent") {
+ if (message.content !== undefined) {
+ console.log("Setting content:", message.content)
+ // Use functional updates to access the current state
+ setIsEditing((currentIsEditing) => {
+ if (currentIsEditing) {
+ // This is a save confirmation
+ setContent(message.content)
+ setEditedContent(message.content)
+ console.log("Save operation completed successfully")
+ return false // Exit edit mode
+ } else {
+ // Initial file load
+ setContent(message.content)
+ setEditedContent(message.content)
+ return currentIsEditing // Keep current edit state
+ }
+ })
+ setError(null)
+ }
+ } else if (message.type === "error") {
+ console.log("Received error:", message.error)
+ setIsEditing((currentIsEditing) => {
+ if (!currentIsEditing) {
+ // Only clear content if this was a read operation
+ console.log("Read operation failed, clearing content")
+ setContent("")
+ setEditedContent("")
+ } else {
+ console.log("Save operation failed")
+ }
+ return currentIsEditing // Keep current edit state
+ })
+ setError(message.error || "Operation failed")
+ }
+ }, []) // Remove isEditing from dependencies
+
+ // Log when the component mounts and unmounts
+ useEffect(() => {
+ console.log("SplitView mounted with filepath:", filePath)
+ return () => {
+ console.log("SplitView unmounted")
+ }
+ }, [filePath])
+
+ useEffect(() => {
+ console.log("Setting up message listener and initiating file read")
+ window.addEventListener("message", handleMessage)
+
+ // Read the file
+ console.log("Sending readWorkspaceFile message for path:", filePath)
+ try {
+ vscode.postMessage({
+ type: "readWorkspaceFile",
+ values: {
+ relativePath: filePath,
+ create: true,
+ ensureDirectory: true,
+ content: "",
+ },
+ })
+ console.log("Successfully sent readWorkspaceFile message")
+ } catch (error) {
+ console.error("Error sending readWorkspaceFile message:", error)
+ setError("Failed to send file read request")
+ }
+
+ // Cleanup
+ return () => {
+ console.log("Cleaning up message listener")
+ window.removeEventListener("message", handleMessage)
+ }
+ }, [filePath, handleMessage])
+
+ const handleSave = useCallback(() => {
+ console.log("Initiating save for file:", filePath)
+ console.log("Content length:", editedContent.length)
+
+ try {
+ // Don't exit edit mode until we get confirmation
+ const message: WebviewMessage = {
+ type: "writeWorkspaceFile",
+ values: {
+ relativePath: filePath,
+ create: true,
+ ensureDirectory: true,
+ content: editedContent,
+ },
+ }
+ vscode.postMessage(message)
+ console.log("Successfully sent writeWorkspaceFile message")
+ } catch (error) {
+ console.error("Error sending writeWorkspaceFile message:", error)
+ setError("Failed to send file save request")
+ }
+ }, [filePath, editedContent])
+
+ const language = getLanguageFromPath(normalizedPath)
+
+ return (
+
+
+ {normalizedPath}
+ {isEditing ? (
+ <>
+ Save
+ setIsEditing(false)}>Cancel
+ >
+ ) : (
+ setIsEditing(true)}>Edit
+ )}
+ Close
+
+
+ {error && !isEditing && (
+
)
diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx
index adb6b47343d..778555f94e9 100644
--- a/webview-ui/src/components/mcp/McpView.tsx
+++ b/webview-ui/src/components/mcp/McpView.tsx
@@ -63,7 +63,7 @@ const McpView = ({ onDone }: McpViewProps) => {
Model Context Protocol
{" "}
enables communication with locally running MCP servers that provide additional tools and resources
- to extend Roo's capabilities. You can use{" "}
+ to extend Agent's capabilities. You can use{" "}
community-made servers
{" "}
@@ -91,7 +91,7 @@ const McpView = ({ onDone }: McpViewProps) => {
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Roo can help you create new MCP servers via commands like "add a new tool
- to...". If you don't need to create MCP servers you can disable this to reduce Roo's
+ to...". If you don't need to create MCP servers you can disable this to reduce Agent's
token usage.
diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx
index 061fa789de4..31a7dd5b115 100644
--- a/webview-ui/src/components/prompts/PromptsView.tsx
+++ b/webview-ui/src/components/prompts/PromptsView.tsx
@@ -688,7 +688,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)}
- Define Roo's expertise and personality for this mode. This description shapes how Roo
+ Define Agent's expertise and personality for this mode. This description shapes how Roo
presents itself and approaches tasks.