From a360d1007e073d935519e884bdc28f98984fea8d Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Tue, 9 Jul 2024 08:57:39 -0400 Subject: [PATCH] Use vscode scrollbar style; add TaskHeader --- webview-ui/package-lock.json | 25 +++++ webview-ui/package.json | 2 + webview-ui/src/App.tsx | 87 +++++++++++++++- webview-ui/src/components/ChatRow.tsx | 9 +- webview-ui/src/components/ChatView.tsx | 127 +++++++---------------- webview-ui/src/components/TaskHeader.tsx | 84 +++++++++++++++ webview-ui/src/index.css | 63 ++++++++++- 7 files changed, 294 insertions(+), 103 deletions(-) create mode 100644 webview-ui/src/components/TaskHeader.tsx diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index d1aa14f..0b10c93 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -19,12 +19,14 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", + "react-text-truncate": "^0.19.0", "react-textarea-autosize": "^8.5.3", "rewire": "^7.0.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, "devDependencies": { + "@types/react-text-truncate": "^0.14.4", "@types/vscode-webview": "^1.57.5" } }, @@ -4562,6 +4564,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-text-truncate": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@types/react-text-truncate/-/react-text-truncate-0.14.4.tgz", + "integrity": "sha512-qdw8522RqdYkTX0FShDPDx8hIRVjPydW8PXl/wKpPGpAtjJTsaNiFOe0fxMRLXIEQaAZvC5VLlKGGONAetb6nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -16143,6 +16155,19 @@ } } }, + "node_modules/react-text-truncate": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.19.0.tgz", + "integrity": "sha512-QxHpZABfGG0Z3WEYbRTZ+rXdZn50Zvp+sWZXgVAd7FCKAMzv/kcwctTpNmWgXDTpAoHhMjOVwmgRtX3x5yeF4w==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.7" + }, + "peerDependencies": { + "react": "^15.4.1 || ^16.0.0 || ^17.0.0 || || ^18.0.0", + "react-dom": "^15.4.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-textarea-autosize": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 66eddc2..00114c4 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -14,6 +14,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-scripts": "5.0.1", + "react-text-truncate": "^0.19.0", "react-textarea-autosize": "^8.5.3", "rewire": "^7.0.0", "typescript": "^4.9.5", @@ -44,6 +45,7 @@ ] }, "devDependencies": { + "@types/react-text-truncate": "^0.14.4", "@types/vscode-webview": "^1.57.5" } } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 681fc21..f6a00ce 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -53,6 +53,91 @@ const App: React.FC = () => { }) }, []) + // dummy data for messages + const generateRandomTimestamp = (baseDate: Date, rangeInDays: number): number => { + const rangeInMs = rangeInDays * 24 * 60 * 60 * 1000 // convert days to milliseconds + const randomOffset = Math.floor(Math.random() * rangeInMs * 2) - rangeInMs // rangeInMs * 2 to have offset in both directions + return baseDate.getTime() + randomOffset + } + + const baseDate = new Date("2024-07-08T00:00:00Z") + + const messages: ClaudeMessage[] = [ + { + type: "say", + say: "task", + text: "Starting task, this is my requeirements", + ts: generateRandomTimestamp(baseDate, 1), + }, + { + type: "ask", + ask: "request_limit_reached", + text: "Request limit reached", + ts: generateRandomTimestamp(baseDate, 2), + }, + { type: "ask", ask: "followup", text: "Any additional questions?", ts: generateRandomTimestamp(baseDate, 3) }, + { type: "say", say: "error", text: "An error occurred", ts: generateRandomTimestamp(baseDate, 4) }, + + { type: "say", say: "text", text: "Some general text", ts: generateRandomTimestamp(baseDate, 7) }, + { type: "say", say: "tool", text: "Using a tool", ts: generateRandomTimestamp(baseDate, 8) }, + + // First command sequence + { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 9) }, + { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 10) }, + { + type: "say", + say: "api_req_started", + text: JSON.stringify({ request: "GET /api/data" }), + ts: generateRandomTimestamp(baseDate, 5), + }, + { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 11) }, + { type: "say", say: "command_output", text: "directory1", ts: generateRandomTimestamp(baseDate, 12) }, + + { type: "say", say: "text", text: "Interrupting text", ts: generateRandomTimestamp(baseDate, 13) }, + { + type: "say", + say: "api_req_finished", + text: JSON.stringify({ cost: "GET /api/data" }), + ts: generateRandomTimestamp(baseDate, 6), + }, + // Second command sequence + { type: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 14) }, + { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 15) }, + + { type: "ask", ask: "completion_result", text: "Task completed", ts: generateRandomTimestamp(baseDate, 16) }, + + // Third command sequence (no output) + { type: "ask", ask: "command", text: "echo Hello", ts: generateRandomTimestamp(baseDate, 17) }, + + // Testing combineApiRequests + { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 18) }, + { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 19) }, + { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 20) }, + { + type: "say", + say: "api_req_started", + text: JSON.stringify({ request: "GET /api/data" }), + ts: generateRandomTimestamp(baseDate, 23), + }, + { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 24) }, + { type: "say", say: "text", text: "Some random text", ts: generateRandomTimestamp(baseDate, 25) }, + { + type: "say", + say: "api_req_finished", + text: JSON.stringify({ cost: 0.005 }), + ts: generateRandomTimestamp(baseDate, 26), + }, + { type: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 27) }, + { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 28) }, + { + type: "say", + say: "api_req_started", + text: JSON.stringify({ request: "POST /api/update" }), + ts: generateRandomTimestamp(baseDate, 29), + }, + { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 30) }, + ] + return ( <> {showWelcome ? ( @@ -66,7 +151,7 @@ const App: React.FC = () => { onDone={() => setShowSettings(false)} /> ) : ( - + )} ) diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index 56691a4..07cd491 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -4,11 +4,11 @@ import { VSCodeButton, VSCodeProgressRing, VSCodeTag } from "@vscode/webview-ui- interface ChatRowProps { message: ClaudeMessage - cost?: string } -const ChatRow: React.FC = ({ message, cost }) => { +const ChatRow: React.FC = ({ message }) => { const [isExpanded, setIsExpanded] = useState(false) + const cost = message.text != null && message.say === "api_req_started" ? JSON.parse(message.text).cost : undefined const getIconAndTitle = (type: ClaudeAsk | ClaudeSay | undefined): [JSX.Element | null, string | null] => { switch (type) { @@ -209,11 +209,6 @@ const ChatRow: React.FC = ({ message, cost }) => {
{renderContent()} {isExpanded && message.say === "api_req_started" && ( diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index 9b1b2ff..e6a9b5b 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -7,82 +7,15 @@ import { ClaudeAskResponse } from "@shared/WebviewMessage" import ChatRow from "./ChatRow" import { combineCommandSequences } from "../utilities/combineCommandSequences" import { combineApiRequests } from "../utilities/combineApiRequests" +import TaskHeader from "./TaskHeader" interface ChatViewProps { messages: ClaudeMessage[] } // maybe instead of storing state in App, just make chatview always show so dont conditionally load/unload? need to make sure messages are persisted (i remember seeing something about how webviews can be frozen in docs) -const ChatView = ({}: ChatViewProps) => { - // dummy data for messages - const generateRandomTimestamp = (baseDate: Date, rangeInDays: number): number => { - const rangeInMs = rangeInDays * 24 * 60 * 60 * 1000 // convert days to milliseconds - const randomOffset = Math.floor(Math.random() * rangeInMs * 2) - rangeInMs // rangeInMs * 2 to have offset in both directions - return baseDate.getTime() + randomOffset - } - - const baseDate = new Date("2024-07-08T00:00:00Z") - - const messages: ClaudeMessage[] = [ - { type: "say", say: "task", text: "Starting task", ts: generateRandomTimestamp(baseDate, 1) }, - { - type: "ask", - ask: "request_limit_reached", - text: "Request limit reached", - ts: generateRandomTimestamp(baseDate, 2), - }, - { type: "ask", ask: "followup", text: "Any additional questions?", ts: generateRandomTimestamp(baseDate, 3) }, - { type: "say", say: "error", text: "An error occurred", ts: generateRandomTimestamp(baseDate, 4) }, - - { type: "say", say: "text", text: "Some general text", ts: generateRandomTimestamp(baseDate, 7) }, - { type: "say", say: "tool", text: "Using a tool", ts: generateRandomTimestamp(baseDate, 8) }, - - // First command sequence - { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 9) }, - { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 10) }, - { type: "say", say: "api_req_started", text: JSON.stringify({ request: "GET /api/data" }), ts: generateRandomTimestamp(baseDate, 5) }, - { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 11) }, - { type: "say", say: "command_output", text: "directory1", ts: generateRandomTimestamp(baseDate, 12) }, - - { type: "say", say: "text", text: "Interrupting text", ts: generateRandomTimestamp(baseDate, 13) }, - { type: "say", say: "api_req_finished", text: JSON.stringify({ cost: "GET /api/data" }), ts: generateRandomTimestamp(baseDate, 6) }, - // Second command sequence - { type: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 14) }, - { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 15) }, - - { type: "ask", ask: "completion_result", text: "Task completed", ts: generateRandomTimestamp(baseDate, 16) }, - - // Third command sequence (no output) - { type: "ask", ask: "command", text: "echo Hello", ts: generateRandomTimestamp(baseDate, 17) }, - - // Testing combineApiRequests - { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 18) }, - { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 19) }, - { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 20) }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ request: "GET /api/data" }), - ts: generateRandomTimestamp(baseDate, 23), - }, - { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 24) }, - { type: "say", say: "text", text: "Some random text", ts: generateRandomTimestamp(baseDate, 25) }, - { - type: "say", - say: "api_req_finished", - text: JSON.stringify({ cost: 0.005 }), - ts: generateRandomTimestamp(baseDate, 26), - }, - { type: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 27) }, - { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 28) }, - { - type: "say", - say: "api_req_started", - text: JSON.stringify({ request: "POST /api/update" }), - ts: generateRandomTimestamp(baseDate, 29), - }, - { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 30) }, - ] - +const ChatView = ({ messages }: ChatViewProps) => { + + const task = messages.shift() const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages)), [messages]) const [inputValue, setInputValue] = useState("") @@ -93,9 +26,9 @@ const ChatView = ({}: ChatViewProps) => { const [claudeAsk, setClaudeAsk] = useState(undefined) - const scrollToBottom = () => { + const scrollToBottom = (instant: boolean = false) => { // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move - messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" }) + (messagesEndRef.current as any)?.scrollIntoView({ behavior: instant ? "instant" : "smooth", block: "nearest", inline: "start" }) } useEffect(() => { @@ -160,52 +93,62 @@ const ChatView = ({}: ChatViewProps) => { return (
-
- {modifiedMessages.map((message) => ( - + +
+ {modifiedMessages.map((message, index) => ( + ))}
-
+
setInputValue(e.target.value)} onKeyDown={handleKeyDown} - onHeightChange={() => scrollToBottom()} + onHeightChange={() => scrollToBottom(true)} placeholder="Type a message..." 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)", + backgroundColor: "var(--vscode-input-background)", + color: "var(--vscode-input-foreground)", + border: "1px solid var(--vscode-input-border)", 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)", + fontFamily: "var(--vscode-font-family)", + fontSize: "var(--vscode-editor-font-size)", + lineHeight: "var(--vscode-editor-line-height)", resize: "none", overflow: "hidden", - paddingTop: "8px", - paddingBottom: "8px", - paddingLeft: "8px", - paddingRight: "40px", // Make room for button + padding: "8px 40px 8px 8px", }} /> {textAreaHeight && (
= ({ taskText, tokensIn, tokensOut, totalCost }) => { + const [isExpanded, setIsExpanded] = useState(false) + const toggleExpand = () => setIsExpanded(!isExpanded) + + return ( +
+
+
+ + See more + + } + /> + {isExpanded && ( + + See less + + )} +
+
+
+ Tokens: +
+ + + {tokensIn.toLocaleString()} + + + + {tokensOut.toLocaleString()} + +
+
+
+ API Cost: + ${totalCost.toFixed(4)} +
+
+
+
+ ) +} + +export default TaskHeader diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index bf8fc96..c6b3cc2 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -9,9 +9,7 @@ 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); } @@ -19,3 +17,62 @@ textarea:focus { vscode-button::part(control):focus { outline: none; } + +/* +Use vscode native scrollbar styles +https://github.com/gitkraken/vscode-gitlens/blob/b1d71d4844523e8b2ef16f9e007068e91f46fd88/src/webviews/apps/home/home.scss +*/ + +html { + height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; +} + +body.scrollable, +.scrollable { + border-color: transparent; + transition: border-color 0.7s linear; +} + +body:hover.scrollable, +body:hover .scrollable, +body:focus-within.scrollable, +body:focus-within .scrollable { + border-color: var(--vscode-scrollbarSlider-background); + transition: none; +} + +::-webkit-scrollbar-corner { + background-color: transparent !important; +} + +::-webkit-scrollbar-thumb { + background-color: transparent; + border-color: inherit; + border-right-style: inset; + border-right-width: calc(100vw + 100vh); + border-radius: unset !important; +} + +::-webkit-scrollbar-thumb:hover { + border-color: var(--vscode-scrollbarSlider-hoverBackground); +} + +::-webkit-scrollbar-thumb:active { + border-color: var(--vscode-scrollbarSlider-activeBackground); +} + +/* +Fix VSCode ignoring webkit scrollbar modifiers +https://github.com/microsoft/vscode/issues/213045 +*/ +@supports selector(::-webkit-scrollbar) { + html { + scrollbar-color: unset; + } +} \ No newline at end of file