mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add new vscode shell integration to run commands right in terminal
This commit is contained in:
1
webview-ui/package-lock.json
generated
1
webview-ui/package-lock.json
generated
@@ -27,7 +27,6 @@
|
||||
"react-virtuoso": "^4.7.13",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rewire": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"styled-components": "^6.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"react-virtuoso": "^4.7.13",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rewire": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"styled-components": "^6.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, useMemo } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
||||
import CodeAccordian from "./CodeAccordian"
|
||||
import CodeBlock from "./CodeBlock"
|
||||
import Terminal from "./Terminal"
|
||||
import Thumbnails from "./Thumbnails"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
|
||||
interface ChatRowProps {
|
||||
message: ClaudeMessage
|
||||
@@ -15,7 +14,6 @@ interface ChatRowProps {
|
||||
onToggleExpand: () => void
|
||||
lastModifiedMessage?: ClaudeMessage
|
||||
isLast: boolean
|
||||
handleSendStdin: (text: string) => void
|
||||
}
|
||||
|
||||
const ChatRow = memo(
|
||||
@@ -36,14 +34,7 @@ const ChatRow = memo(
|
||||
|
||||
export default ChatRow
|
||||
|
||||
const ChatRowContent = ({
|
||||
message,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
lastModifiedMessage,
|
||||
isLast,
|
||||
handleSendStdin,
|
||||
}: ChatRowProps) => {
|
||||
const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowProps) => {
|
||||
const cost = useMemo(() => {
|
||||
if (message.text != null && message.say === "api_req_started") {
|
||||
return JSON.parse(message.text).cost
|
||||
@@ -483,7 +474,29 @@ const ChatRowContent = ({
|
||||
}
|
||||
return {
|
||||
command: text.slice(0, outputIndex).trim(),
|
||||
output: text.slice(outputIndex + COMMAND_OUTPUT_STRING.length).trim() + " ",
|
||||
output: text
|
||||
.slice(outputIndex + COMMAND_OUTPUT_STRING.length)
|
||||
.trim()
|
||||
.split("")
|
||||
.map((char) => {
|
||||
switch (char) {
|
||||
case "\n":
|
||||
return "↵\n"
|
||||
case "\r":
|
||||
return "⏎"
|
||||
case "\t":
|
||||
return "→ "
|
||||
case "\b":
|
||||
return "⌫"
|
||||
case "\f":
|
||||
return "⏏"
|
||||
case "\v":
|
||||
return "⇳"
|
||||
default:
|
||||
return char
|
||||
}
|
||||
})
|
||||
.join(""),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,11 +507,28 @@ const ChatRowContent = ({
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<Terminal
|
||||
{/* <Terminal
|
||||
rawOutput={command + (output ? "\n" + output : "")}
|
||||
handleSendStdin={handleSendStdin}
|
||||
shouldAllowInput={!!isCommandExecuting && output.length > 0}
|
||||
/>
|
||||
/> */}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
border: "1px solid var(--vscode-sideBar-border)",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} />
|
||||
</div>
|
||||
{output.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
border: "1px solid var(--vscode-sideBar-border)",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEvent, useMount } from "react-use"
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||
import { ClaudeAsk, ClaudeSayTool, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
|
||||
import { combineApiRequests } from "../../../src/shared/combineApiRequests"
|
||||
import { combineCommandSequences, COMMAND_STDIN_STRING } from "../../../src/shared/combineCommandSequences"
|
||||
import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
|
||||
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
|
||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||
import { vscode } from "../utils/vscode"
|
||||
@@ -118,7 +118,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setTextAreaDisabled(false)
|
||||
setClaudeAsk("command_output")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Exit Command")
|
||||
setPrimaryButtonText("Proceed While Running")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "completion_result":
|
||||
@@ -224,23 +224,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
}
|
||||
}, [inputValue, selectedImages, messages.length, claudeAsk])
|
||||
|
||||
const handleSendStdin = useCallback(
|
||||
(text: string) => {
|
||||
if (claudeAsk === "command_output") {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
text: COMMAND_STDIN_STRING + text,
|
||||
})
|
||||
setClaudeAsk(undefined)
|
||||
// don't need to disable since extension relinquishes control back immediately
|
||||
// setTextAreaDisabled(true)
|
||||
// setEnableButtons(false)
|
||||
}
|
||||
},
|
||||
[claudeAsk]
|
||||
)
|
||||
|
||||
const startNewTask = useCallback(() => {
|
||||
vscode.postMessage({ type: "clearTask" })
|
||||
}, [])
|
||||
@@ -468,10 +451,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === visibleMessages.length - 1}
|
||||
handleSendStdin={handleSendStdin}
|
||||
/>
|
||||
),
|
||||
[expandedRows, modifiedMessages, visibleMessages.length, handleSendStdin]
|
||||
[expandedRows, modifiedMessages, visibleMessages.length]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useMemo, memo } from "react"
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
import stripAnsi from "strip-ansi"
|
||||
|
||||
interface TerminalProps {
|
||||
rawOutput: string
|
||||
handleSendStdin: (text: string) => void
|
||||
shouldAllowInput: boolean
|
||||
}
|
||||
|
||||
/*
|
||||
Inspired by https://phuoc.ng/collection/mirror-a-text-area/create-your-own-custom-cursor-in-a-text-area/
|
||||
|
||||
Note: Even though vscode exposes var(--vscode-terminalCursor-foreground) it does not render in front of a color that isn't var(--vscode-terminal-background), and it turns out a lot of themes don't even define some/any of these terminal color variables. Very odd behavior, so try changing themes/color variables if you don't see the caret.
|
||||
*/
|
||||
|
||||
const Terminal = ({ rawOutput, handleSendStdin, shouldAllowInput }: TerminalProps) => {
|
||||
const [userInput, setUserInput] = useState("")
|
||||
const [isFocused, setIsFocused] = useState(false) // Initially not focused
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const mirrorRef = useRef<HTMLDivElement>(null)
|
||||
const hiddenTextareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const [lastProcessedOutput, setLastProcessedOutput] = useState("")
|
||||
|
||||
const output = useMemo(() => {
|
||||
return stripAnsi(rawOutput)
|
||||
}, [rawOutput])
|
||||
|
||||
useEffect(() => {
|
||||
if (lastProcessedOutput !== output) {
|
||||
setUserInput("")
|
||||
}
|
||||
}, [output, lastProcessedOutput])
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
handleSendStdin(userInput)
|
||||
// setUserInput("") // Clear user input after processing
|
||||
setLastProcessedOutput(output)
|
||||
|
||||
// Trigger resize after clearing input
|
||||
const textarea = textAreaRef.current
|
||||
const hiddenTextarea = hiddenTextareaRef.current
|
||||
if (textarea && hiddenTextarea) {
|
||||
hiddenTextarea.value = ""
|
||||
const newHeight = hiddenTextarea.scrollHeight
|
||||
textarea.style.height = `${newHeight}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setUserInput("") // Reset user input when output changes
|
||||
}, [output])
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current
|
||||
const mirror = mirrorRef.current
|
||||
const hiddenTextarea = hiddenTextareaRef.current
|
||||
if (!textarea || !mirror || !hiddenTextarea) return
|
||||
|
||||
const textareaStyles = window.getComputedStyle(textarea)
|
||||
const stylesToCopy = [
|
||||
"border",
|
||||
"boxSizing",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontWeight",
|
||||
"letterSpacing",
|
||||
"lineHeight",
|
||||
"padding",
|
||||
"textDecoration",
|
||||
"textIndent",
|
||||
"textTransform",
|
||||
"whiteSpace",
|
||||
"wordSpacing",
|
||||
"wordWrap",
|
||||
"width",
|
||||
"height",
|
||||
]
|
||||
|
||||
stylesToCopy.forEach((property) => {
|
||||
mirror.style[property as any] = textareaStyles[property as any]
|
||||
hiddenTextarea.style[property as any] = textareaStyles[property as any]
|
||||
})
|
||||
mirror.style.borderColor = "transparent"
|
||||
hiddenTextarea.style.visibility = "hidden"
|
||||
hiddenTextarea.style.position = "absolute"
|
||||
// hiddenTextarea.style.height = "auto"
|
||||
hiddenTextarea.style.width = `${textarea.clientWidth}px`
|
||||
hiddenTextarea.style.whiteSpace = "pre-wrap"
|
||||
hiddenTextarea.style.overflowWrap = "break-word"
|
||||
|
||||
// const borderWidth = parseInt(textareaStyles.borderWidth, 10) || 0
|
||||
const updateSize = () => {
|
||||
hiddenTextarea.value = textarea.value
|
||||
const newHeight = hiddenTextarea.scrollHeight
|
||||
textarea.style.height = `${newHeight}px`
|
||||
mirror.style.width = `${textarea.offsetWidth}px`
|
||||
mirror.style.height = `${newHeight}px`
|
||||
hiddenTextarea.style.width = `${textarea.offsetWidth}px`
|
||||
hiddenTextarea.style.height = `${newHeight}px`
|
||||
}
|
||||
|
||||
updateSize()
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateSize)
|
||||
resizeObserver.observe(textarea)
|
||||
|
||||
// Add window resize event listener
|
||||
const handleWindowResize = () => {
|
||||
hiddenTextarea.style.width = `${textarea.clientWidth}px`
|
||||
updateSize()
|
||||
}
|
||||
window.addEventListener("resize", handleWindowResize)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
window.removeEventListener("resize", handleWindowResize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current
|
||||
const mirror = mirrorRef.current
|
||||
if (!textarea || !mirror) return
|
||||
|
||||
const handleScroll = () => {
|
||||
if (mirror) mirror.scrollTop = textarea.scrollTop
|
||||
}
|
||||
|
||||
textarea.addEventListener("scroll", handleScroll)
|
||||
return () => textarea.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current
|
||||
const mirror = mirrorRef.current
|
||||
if (!textarea || !mirror) return
|
||||
|
||||
const updateMirror = () => {
|
||||
const cursorPos = textarea.selectionStart
|
||||
const textBeforeCursor = textarea.value.substring(0, cursorPos)
|
||||
const textAfterCursor = textarea.value.substring(cursorPos)
|
||||
|
||||
mirror.innerHTML = ""
|
||||
mirror.appendChild(document.createTextNode(textBeforeCursor))
|
||||
|
||||
const caretEle = document.createElement("span")
|
||||
caretEle.classList.add("terminal-cursor")
|
||||
if (isFocused) {
|
||||
caretEle.classList.add("terminal-cursor-focused")
|
||||
}
|
||||
if (!shouldAllowInput) {
|
||||
caretEle.classList.add("terminal-cursor-hidden")
|
||||
}
|
||||
caretEle.innerHTML = " "
|
||||
mirror.appendChild(caretEle)
|
||||
|
||||
mirror.appendChild(document.createTextNode(textAfterCursor))
|
||||
}
|
||||
|
||||
// Update mirror on initial render
|
||||
updateMirror()
|
||||
|
||||
document.addEventListener("selectionchange", updateMirror)
|
||||
return () => document.removeEventListener("selectionchange", updateMirror)
|
||||
}, [userInput, isFocused, shouldAllowInput])
|
||||
|
||||
useEffect(() => {
|
||||
// Position the dummy caret at the end of the text on initial render
|
||||
const mirror = mirrorRef.current
|
||||
if (mirror) {
|
||||
const text = output + userInput
|
||||
mirror.innerHTML = ""
|
||||
mirror.appendChild(document.createTextNode(text))
|
||||
|
||||
const caretEle = document.createElement("span")
|
||||
caretEle.classList.add("terminal-cursor")
|
||||
if (isFocused) {
|
||||
caretEle.classList.add("terminal-cursor-focused")
|
||||
}
|
||||
if (!shouldAllowInput) {
|
||||
caretEle.classList.add("terminal-cursor-hidden")
|
||||
}
|
||||
caretEle.innerHTML = " "
|
||||
mirror.appendChild(caretEle)
|
||||
}
|
||||
}, [output, userInput, isFocused, shouldAllowInput])
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = e.target.value
|
||||
|
||||
// Ensure the user can only edit their input after the output
|
||||
if (newValue.startsWith(output)) {
|
||||
setUserInput(newValue.slice(output.length))
|
||||
} else {
|
||||
// If the user tries to edit the output part, reset the value to the correct state
|
||||
e.target.value = output + userInput
|
||||
}
|
||||
|
||||
// Trigger resize after setting user input
|
||||
const textarea = textAreaRef.current
|
||||
const hiddenTextarea = hiddenTextareaRef.current
|
||||
if (textarea && hiddenTextarea) {
|
||||
hiddenTextarea.value = output + userInput
|
||||
const newHeight = hiddenTextarea.scrollHeight
|
||||
textarea.style.height = `${newHeight}px`
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const textarea = e.target as HTMLTextAreaElement
|
||||
const cursorPosition = textarea.selectionStart
|
||||
|
||||
// Prevent backspace from deleting the output part
|
||||
if (e.key === "Backspace" && cursorPosition <= output.length) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Update cursor position on backspace
|
||||
setTimeout(() => {
|
||||
const cursorPos = textarea.selectionStart
|
||||
const textBeforeCursor = textarea.value.substring(0, cursorPos)
|
||||
const textAfterCursor = textarea.value.substring(cursorPos)
|
||||
|
||||
mirrorRef.current!.innerHTML = ""
|
||||
mirrorRef.current!.appendChild(document.createTextNode(textBeforeCursor))
|
||||
|
||||
const caretEle = document.createElement("span")
|
||||
caretEle.classList.add("terminal-cursor")
|
||||
if (isFocused) {
|
||||
caretEle.classList.add("terminal-cursor-focused")
|
||||
}
|
||||
if (!shouldAllowInput) {
|
||||
caretEle.classList.add("terminal-cursor-hidden")
|
||||
}
|
||||
caretEle.innerHTML = " "
|
||||
mirrorRef.current!.appendChild(caretEle)
|
||||
|
||||
mirrorRef.current!.appendChild(document.createTextNode(textAfterCursor))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const textAreaStyle: React.CSSProperties = {
|
||||
fontFamily: "var(--vscode-editor-font-family)",
|
||||
fontSize: "var(--vscode-editor-font-size)",
|
||||
padding: "10px",
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
outline: "none",
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
resize: "none",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="terminal-container">
|
||||
<style>
|
||||
{`
|
||||
.terminal-container {
|
||||
position: relative;
|
||||
overflow: hidden; // Add this
|
||||
}
|
||||
|
||||
.terminal-textarea {
|
||||
background: transparent;
|
||||
caret-color: transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.terminal-mirror {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
color: transparent;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.terminal-cursor {
|
||||
border: 1px solid var(--vscode-terminal-foreground, #FFFFFF);
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
margin-top: -0.5px;
|
||||
}
|
||||
|
||||
.terminal-cursor-focused {
|
||||
background-color: var(--vscode-terminal-foreground, #FFFFFF);
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
.terminal-cursor-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<DynamicTextArea
|
||||
ref={textAreaRef}
|
||||
value={output + (shouldAllowInput ? userInput : "")}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyPress={handleKeyPress}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
className="terminal-textarea"
|
||||
style={{
|
||||
// backgroundColor: "var(--vscode-editor-background)", // NOTE: adding cursor ontop of this color wouldnt work on some themes
|
||||
caretColor: "transparent", // Hide default caret
|
||||
color: "var(--vscode-terminal-foreground)",
|
||||
borderRadius: "3px",
|
||||
...(textAreaStyle as any),
|
||||
}}
|
||||
minRows={1}
|
||||
/>
|
||||
<div ref={mirrorRef} className="terminal-mirror"></div>
|
||||
<DynamicTextArea
|
||||
ref={hiddenTextareaRef}
|
||||
className="terminal-textarea"
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
minRows={1}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
opacity: 0,
|
||||
...(textAreaStyle as any),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Terminal)
|
||||
Reference in New Issue
Block a user