2
0
Эх сурвалжийг харах

Make react build task; add react-textarea-autosize; add settings button; add settings page; get basic chat interface working

Saoud Rizwan 1 жил өмнө
parent
commit
08effc4799

+ 13 - 0
.vscode/tasks.json

@@ -6,6 +6,7 @@
 		{
             "label": "watch",
             "dependsOn": [
+                "npm: build:webview",
                 "npm: watch:tsc",
                 "npm: watch:esbuild"
             ],
@@ -17,6 +18,18 @@
                 "isDefault": true
             }
         },
+        {
+            "type": "npm",
+            "script": "build:webview",
+            "group": "build",
+            "problemMatcher": [],
+            "isBackground": true,
+            "label": "npm: build:webview",
+            "presentation": {
+                "group": "watch",
+                "reveal": "never"
+            }
+        },
         {
             "type": "npm",
             "script": "watch:esbuild",

+ 14 - 4
package.json

@@ -32,15 +32,25 @@
     },
     "commands": [
       {
-        "command": "claude-dev.menuButtonTapped",
-        "title": "Text that will show when hovered",
-        "icon": "$(clear-all)"
+        "command": "claude-dev.plusButtonTapped",
+        "title": "New Task",
+        "icon": "$(add)"
+      },
+      {
+        "command": "claude-dev.settingsButtonTapped",
+        "title": "Settings",
+        "icon": "$(settings-gear)"
       }
     ],
     "menus": {
       "view/title": [
         {
-          "command": "claude-dev.menuButtonTapped",
+          "command": "claude-dev.plusButtonTapped",
+          "group": "navigation",
+          "when": "view == claude-dev.SidebarProvider"
+        },
+        {
+          "command": "claude-dev.settingsButtonTapped",
           "group": "navigation",
           "when": "view == claude-dev.SidebarProvider"
         }

+ 9 - 2
src/extension.ts

@@ -34,8 +34,15 @@ export function activate(context: vscode.ExtensionContext) {
 	context.subscriptions.push(vscode.window.registerWebviewViewProvider(SidebarProvider.viewType, provider))
 
 	context.subscriptions.push(
-		vscode.commands.registerCommand("claude-dev.menuButtonTapped", () => {
-			const message = "claude-dev.menuButtonTapped!"
+		vscode.commands.registerCommand("claude-dev.plusButtonTapped", () => {
+			const message = "claude-dev.plusButtonTapped!"
+			vscode.window.showInformationMessage(message)
+		})
+	)
+
+	context.subscriptions.push(
+		vscode.commands.registerCommand("claude-dev.settingsButtonTapped", () => {
+			const message = "claude-dev.settingsButtonTapped!"
 			vscode.window.showInformationMessage(message)
 		})
 	)

+ 58 - 0
webview-ui/package-lock.json

@@ -19,6 +19,7 @@
         "react": "^18.3.1",
         "react-dom": "^18.3.1",
         "react-scripts": "5.0.1",
+        "react-textarea-autosize": "^8.5.3",
         "rewire": "^7.0.0",
         "typescript": "^4.9.5",
         "web-vitals": "^2.1.4"
@@ -16142,6 +16143,23 @@
         }
       }
     },
+    "node_modules/react-textarea-autosize": {
+      "version": "8.5.3",
+      "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz",
+      "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.13",
+        "use-composed-ref": "^1.3.0",
+        "use-latest": "^1.2.1"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -18549,6 +18567,46 @@
         "requires-port": "^1.0.0"
       }
     },
+    "node_modules/use-composed-ref": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz",
+      "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/use-isomorphic-layout-effect": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+      "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/use-latest": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz",
+      "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==",
+      "license": "MIT",
+      "dependencies": {
+        "use-isomorphic-layout-effect": "^1.1.1"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

+ 1 - 0
webview-ui/package.json

@@ -14,6 +14,7 @@
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-scripts": "5.0.1",
+    "react-textarea-autosize": "^8.5.3",
     "rewire": "^7.0.0",
     "typescript": "^4.9.5",
     "web-vitals": "^2.1.4"

+ 6 - 8
webview-ui/src/App.tsx

@@ -1,8 +1,7 @@
-import React from "react"
+import React, { useState } from "react"
 import logo from "./logo.svg"
 import "./App.css"
 
-
 import { vscode } from "./utilities/vscode"
 import {
 	VSCodeBadge,
@@ -26,8 +25,12 @@ import {
 	VSCodeTextField,
 } from "@vscode/webview-ui-toolkit/react"
 import ChatSidebar from "./components/ChatSidebar"
+import Demo from "./components/Demo"
+import SettingsView from "./components/SettingsView"
 
 const App: React.FC = () => {
+	const [showSettings, setShowSettings] = useState(true)
+
 	const handleHowdyClick = () => {
 		vscode.postMessage({
 			command: "hello",
@@ -35,12 +38,7 @@ const App: React.FC = () => {
 		})
 	}
 
-	return (
-		// REMOVE COLOR
-		<main style={{backgroundColor: '#232526'}}>
-			<ChatSidebar />
-		</main>
-	)
+	return <>{showSettings ? <SettingsView /> : <ChatSidebar />}</>
 }
 
 export default App

+ 61 - 32
webview-ui/src/components/ChatSidebar.tsx

@@ -1,7 +1,7 @@
-import React, { useState, useRef, useEffect, useCallback } from "react"
+import React, { useState, useRef, useEffect, useCallback, KeyboardEvent } from "react"
 import { VSCodeButton, VSCodeTextArea, VSCodeDivider, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { vscode } from "../utilities/vscode"
-import ResizingTextArea from "./ResizingTextArea"
+import DynamicTextArea from "react-textarea-autosize"
 
 interface Message {
 	id: number
@@ -13,9 +13,12 @@ const ChatSidebar = () => {
 	const [messages, setMessages] = useState<Message[]>([])
 	const [inputValue, setInputValue] = useState("")
 	const messagesEndRef = useRef<HTMLDivElement>(null)
+	const textAreaRef = useRef<HTMLTextAreaElement>(null)
+	const [textAreaHeight, setTextAreaHeight] = useState<number | undefined>(undefined)
 
 	const scrollToBottom = () => {
-		messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
+		// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
+		messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: 'nearest', inline: 'start' })
 	}
 
 	useEffect(scrollToBottom, [messages])
@@ -29,10 +32,6 @@ const ChatSidebar = () => {
 			}
 			setMessages([...messages, newMessage])
 			setInputValue("")
-			// if (textAreaRef.current) {
-			// 	textAreaRef.current.style.height = "auto"
-			// }
-
 			// Here you would typically send the message to your extension's backend
 			vscode.postMessage({
 				command: "sendMessage",
@@ -40,14 +39,25 @@ const ChatSidebar = () => {
 			})
 		}
 	}
+	const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
+		if (event.key === "Enter" && !event.shiftKey) {
+			event.preventDefault()
+			handleSendMessage()
+		}
+	}
+
+	useEffect(() => {
+		if (textAreaRef.current && !textAreaHeight) {
+			setTextAreaHeight(textAreaRef.current.offsetHeight)
+		}
+	}, [])
 
 	return (
-		<div className="chat-sidebar" style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
-			<div className="message-list" style={{ flexGrow: 1, overflowY: "auto", padding: "10px" }}>
+		<div style={{ display: "flex", flexDirection: "column", height: "100vh", backgroundColor: "gray", overflow: "hidden" }}>
+			<div style={{ flexGrow: 1, overflowY: "scroll", scrollbarWidth: "none" }}>
 				{messages.map((message) => (
 					<div
 						key={message.id}
-						className={`message ${message.sender}`}
 						style={{
 							marginBottom: "10px",
 							padding: "8px",
@@ -57,35 +67,54 @@ const ChatSidebar = () => {
 									? "var(--vscode-editor-background)"
 									: "var(--vscode-sideBar-background)",
 						}}>
-						{message.text}
+						<span style={{ whiteSpace: "pre-line", overflowWrap: "break-word" }}>{message.text}</span>
 					</div>
 				))}
-				<div ref={messagesEndRef} />
+				<div style={{ float:"left", clear: "both" }} ref={messagesEndRef} />
 			</div>
-			<VSCodeDivider />
-			<div className="input-area" style={{ padding: 20 }}>
-				<ResizingTextArea
+			<div style={{ position: "relative", paddingTop: "16px", paddingBottom: "16px" }}>
+				<DynamicTextArea
+					ref={textAreaRef}
 					value={inputValue}
-					onChange={setInputValue}
+					onChange={(e) => setInputValue(e.target.value)}
+					onKeyDown={handleKeyDown}
+					onHeightChange={() => scrollToBottom()}
 					placeholder="Type a message..."
-					style={{ marginBottom: "10px", width: "100%" }}
+					maxRows={10}
+					style={{
+						width: "100%",
+						boxSizing: "border-box",
+						backgroundColor: "var(--vscode-input-background, #3c3c3c)",
+						color: "var(--vscode-input-foreground, #cccccc)",
+						border: "1px solid var(--vscode-input-border, #3c3c3c)",
+						borderRadius: "2px",
+						fontFamily:
+							"var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif)",
+						fontSize: "var(--vscode-editor-font-size, 13px)",
+						lineHeight: "var(--vscode-editor-line-height, 1.5)",
+						resize: "none",
+						overflow: "hidden",
+						paddingTop: "8px",
+						paddingBottom: "8px",
+						paddingLeft: "8px",
+						paddingRight: "40px", // Make room for button
+					}}
 				/>
-				<VSCodeButton onClick={handleSendMessage}>Send</VSCodeButton>
-				<VSCodeTextField>
-					<section slot="end" style={{ display: "flex", alignItems: "center" }}>
-						<VSCodeButton appearance="icon" aria-label="Match Case">
-							<span className="codicon codicon-case-sensitive"></span>
-						</VSCodeButton>
-						<VSCodeButton appearance="icon" aria-label="Match Whole Word">
-							<span className="codicon codicon-whole-word"></span>
-						</VSCodeButton>
-						<VSCodeButton appearance="icon" aria-label="Use Regular Expression">
-							<span className="codicon codicon-regex"></span>
+				{textAreaHeight && (
+					<div
+						style={{
+							position: "absolute",
+							right: "12px",
+							height: `${textAreaHeight}px`,
+							bottom: "18px",
+							display: "flex",
+							alignItems: "center",
+						}}>
+						<VSCodeButton appearance="icon" aria-label="Send Message" onClick={handleSendMessage}>
+							<span className="codicon codicon-send"></span>
 						</VSCodeButton>
-					</section>
-				</VSCodeTextField>
-				<span slot="end" className="codicon codicon-chevron-right"></span>
-				<VSCodeButton onClick={handleSendMessage}>Send</VSCodeButton>
+					</div>
+				)}
 			</div>
 		</div>
 	)

+ 0 - 46
webview-ui/src/components/ResizingTextArea.tsx

@@ -1,46 +0,0 @@
-import React, { TextareaHTMLAttributes, CSSProperties, useRef, useEffect } from "react"
-
-interface ResizingTextAreaProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange"> {
-	onChange: (value: string) => void
-}
-
-const ResizingTextArea= ({ style, value, onChange, ...props }: ResizingTextAreaProps) => {
-	const textAreaRef = useRef<HTMLTextAreaElement>(null)
-
-	const textareaStyle: CSSProperties = {
-		width: "100%",
-		minHeight: "60px",
-		backgroundColor: "var(--vscode-input-background, #3c3c3c)",
-		color: "var(--vscode-input-foreground, #cccccc)",
-		border: "1px solid var(--vscode-input-border, #3c3c3c)",
-		borderRadius: "2px",
-		padding: "4px 8px",
-		outline: "none",
-		fontFamily: "var(--vscode-editor-font-family)",
-		fontSize: "var(--vscode-editor-font-size, 13px)",
-		lineHeight: "var(--vscode-editor-line-height, 1.5)",
-		resize: "none",
-		overflow: "hidden",
-		...style,
-	}
-
-	const adjustTextAreaHeight = () => {
-		if (textAreaRef.current) {
-			textAreaRef.current.style.height = "auto"
-			textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`
-		}
-	}
-
-	const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
-		onChange(event.target.value)
-		adjustTextAreaHeight()
-	}
-
-	useEffect(() => {
-		adjustTextAreaHeight()
-	}, [value])
-
-	return <textarea ref={textAreaRef} style={textareaStyle} value={value} onChange={handleInputChange} {...props} />
-}
-
-export default ResizingTextArea

+ 78 - 0
webview-ui/src/components/SettingsView.tsx

@@ -0,0 +1,78 @@
+import React from "react"
+import { VSCodeTextField, VSCodeDivider, VSCodeLink, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
+
+const SettingsView = () => {
+	const handleDoneClick = () => {
+		// Add your logic here for what should happen when the Done button is clicked
+		console.log("Done button clicked")
+	}
+
+	return (
+		<div style={{ margin: "0 auto", paddingTop: "10px" }}>
+			<div
+				style={{
+					display: "flex",
+					justifyContent: "space-between",
+					alignItems: "center",
+					marginBottom: "20px",
+				}}>
+				<h2 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h2>
+				<VSCodeButton onClick={handleDoneClick}>Done</VSCodeButton>
+			</div>
+
+			<div style={{ marginBottom: "20px" }}>
+				<VSCodeTextField style={{ width: "100%" }} placeholder="Enter your Anthropic API Key">
+					Anthropic API Key
+				</VSCodeTextField>
+				<p
+					style={{
+						fontSize: "12px",
+						marginTop: "5px",
+						color: "var(--vscode-descriptionForeground)",
+					}}>
+					This key is not shared with anyone and only used to make API requests from the extension.
+					<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
+						You can get an API key by signing up here.
+					</VSCodeLink>
+				</p>
+			</div>
+
+			<div style={{ marginBottom: "20px" }}>
+				<VSCodeTextField style={{ width: "100%" }} placeholder="Enter maximum number of requests">
+					Maximum # Requests Per Task
+				</VSCodeTextField>
+				<p
+					style={{
+						fontSize: "12px",
+						marginTop: "5px",
+						color: "var(--vscode-descriptionForeground)",
+					}}>
+					If Claude Dev reaches this limit, it will pause and ask for your permission before making additional
+					requests.
+				</p>
+			</div>
+
+			<VSCodeDivider />
+
+			<div
+				style={{
+					marginTop: "20px",
+					textAlign: "center",
+					color: "var(--vscode-descriptionForeground)",
+					fontSize: "12px",
+					lineHeight: "1.5",
+                    fontStyle: "italic"
+				}}>
+				<p>Made possible by the latest breakthroughs in Claude 3.5 Sonnet's agentic coding capabilities.</p>
+				<p>
+					This project was submitted to Anthropic's "Build with Claude June 2024 contest".
+					<VSCodeLink href="https://github.com/saoudrizwan/claude-dev">
+						github.com/saoudrizwan/claude-dev
+					</VSCodeLink>
+				</p>
+			</div>
+		</div>
+	)
+}
+
+export default SettingsView

+ 7 - 1
webview-ui/src/index.css

@@ -1,4 +1,4 @@
-body {
+/* body {
 	margin: 0;
 	font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
 		"Droid Sans", "Helvetica Neue", sans-serif;
@@ -8,4 +8,10 @@ body {
 
 code {
 	font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
+} */
+body {
+	margin: 0;
 }
+textarea:focus {
+	outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
+}