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

This commit is contained in:
Saoud Rizwan
2024-07-07 05:05:28 -04:00
parent b0f93bffe9
commit 08effc4799
10 changed files with 247 additions and 93 deletions

13
.vscode/tasks.json vendored
View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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)
})
)

View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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>
{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>
<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>
</VSCodeButton>
</section>
</VSCodeTextField>
<span slot="end" className="codicon codicon-chevron-right"></span>
<VSCodeButton onClick={handleSendMessage}>Send</VSCodeButton>
</div>
)}
</div>
</div>
)

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}