From 9e5a475a2a13e4c03870a4cfd1bdde64cd90c947 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Tue, 23 Jul 2024 06:29:37 -0400 Subject: [PATCH] Replace event listeners with react-use useEvent; fix bugs with task text truncation, stale effects, and 'visible' extension TypeError --- src/providers/SidebarProvider.ts | 2 +- webview-ui/package-lock.json | 223 +++++++++++++++++++++ webview-ui/package.json | 1 + webview-ui/src/App.tsx | 69 +++---- webview-ui/src/components/ChatView.tsx | 36 ++-- webview-ui/src/components/SettingsView.tsx | 24 ++- webview-ui/src/components/TaskHeader.tsx | 92 +++++++-- 7 files changed, 368 insertions(+), 79 deletions(-) diff --git a/src/providers/SidebarProvider.ts b/src/providers/SidebarProvider.ts index baa666f..bfa2b37 100644 --- a/src/providers/SidebarProvider.ts +++ b/src/providers/SidebarProvider.ts @@ -44,7 +44,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider { // Listen for when the panel becomes visible // https://github.com/microsoft/vscode-discussions/discussions/840 webviewView.onDidChangeVisibility((e: any) => { - if (e.visible) { + if (e && e.visible) { // Your view is visible this.postMessageToWebview({ type: "action", action: "didBecomeVisible" }) } else { diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 9b627cc..d82fe8e 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -23,6 +23,7 @@ "react-syntax-highlighter": "^15.5.0", "react-text-truncate": "^0.19.0", "react-textarea-autosize": "^8.5.3", + "react-use": "^17.5.1", "rewire": "^7.0.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" @@ -4489,6 +4490,11 @@ "pretty-format": "^27.0.0" } }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5121,6 +5127,11 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -6718,6 +6729,14 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js": { "version": "3.37.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", @@ -6846,6 +6865,14 @@ "postcss": "^8.4" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -8895,6 +8922,16 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -10120,6 +10157,11 @@ "node": ">=10.17.0" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -10266,6 +10308,14 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -13116,6 +13166,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13786,6 +13841,50 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-css": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.1", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nano-css/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nano-css/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/nano-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -16455,6 +16554,40 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.1.tgz", + "integrity": "sha512-LG/uPEVRflLWMwi3j/sZqR00nF6JGqTTDblkXK2nzXsIvij06hXl1V/MZIlwj1OKIQUtlh1l9jK8gLsRyCQxMg==", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.2", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -16700,6 +16833,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -16932,6 +17070,14 @@ "node": ">=8" } }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -17140,6 +17286,17 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -17350,6 +17507,14 @@ "node": ">= 0.4" } }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "engines": { + "node": ">=6.9" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -17560,6 +17725,14 @@ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "license": "MIT" }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -17587,6 +17760,33 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "license": "MIT" }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", @@ -17984,6 +18184,11 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -18449,6 +18654,14 @@ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", "license": "MIT" }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -18482,6 +18695,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -18533,6 +18751,11 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", "license": "MIT" }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index a357aac..e0aad28 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -18,6 +18,7 @@ "react-syntax-highlighter": "^15.5.0", "react-text-truncate": "^0.19.0", "react-textarea-autosize": "^8.5.3", + "react-use": "^17.5.1", "rewire": "^7.0.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 1ff8f92..df34c7d 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -1,12 +1,11 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect, useState, useCallback } from "react" import "./App.css" - import ChatView from "./components/ChatView" import SettingsView from "./components/SettingsView" import { ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage" import WelcomeView from "./components/WelcomeView" import { vscode } from "./utilities/vscode" -//import { mockMessages } from "./utilities/mockMessages" +import { useEvent } from "react-use" /* The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab. @@ -26,43 +25,37 @@ const App: React.FC = () => { useEffect(() => { vscode.postMessage({ type: "webviewDidLaunch" }) - - const handleMessage = (e: MessageEvent) => { - const message: ExtensionMessage = e.data - // switch message.type - switch (message.type) { - case "state": - const shouldShowWelcome = !message.state!.didOpenOnce || !message.state!.apiKey - setShowWelcome(shouldShowWelcome) - setApiKey(message.state!.apiKey || "") - setMaxRequestsPerTask( - message.state!.maxRequestsPerTask !== undefined - ? message.state!.maxRequestsPerTask.toString() - : "" - ) - setVscodeThemeName(message.state!.themeName) - setClaudeMessages(message.state!.claudeMessages) - break - case "action": - switch (message.action!) { - case "settingsButtonTapped": - setShowSettings(true) - break - case "plusButtonTapped": - setShowSettings(false) - break - } - break - } - } - - window.addEventListener("message", handleMessage) - - return () => { - window.removeEventListener("message", handleMessage) - } }, []) + const handleMessage = useCallback((e: MessageEvent) => { + const message: ExtensionMessage = e.data + switch (message.type) { + case "state": + const shouldShowWelcome = !message.state!.didOpenOnce || !message.state!.apiKey + setShowWelcome(shouldShowWelcome) + setApiKey(message.state!.apiKey || "") + setMaxRequestsPerTask( + message.state!.maxRequestsPerTask !== undefined ? message.state!.maxRequestsPerTask.toString() : "" + ) + setVscodeThemeName(message.state!.themeName) + setClaudeMessages(message.state!.claudeMessages) + break + case "action": + switch (message.action!) { + case "settingsButtonTapped": + setShowSettings(true) + break + case "plusButtonTapped": + setShowSettings(false) + break + } + break + } + // we don't need to define any dependencies since we're not using any state in the callback. if you were to use state, you'd either have to include it in the dependency array or use the updater function `setUserText(prev => `${prev}${key}`);`. (react-use takes care of not registering the same listener multiple times even if this callback is updated.) + }, []) + + useEvent("message", handleMessage) + return ( <> {showWelcome ? ( diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index 630df97..366fd0e 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -1,6 +1,6 @@ import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage" import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react" +import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react" import { animateScroll as scroll } from "react-scroll" import DynamicTextArea from "react-textarea-autosize" import { combineApiRequests } from "../utilities/combineApiRequests" @@ -11,6 +11,7 @@ import ChatRow from "./ChatRow" import TaskHeader from "./TaskHeader" import { getSyntaxHighlighterStyleFromTheme } from "../utilities/getSyntaxHighlighterStyleFromTheme" import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus" +import { useEvent } from "react-use" interface ChatViewProps { messages: ClaudeMessage[] @@ -242,28 +243,33 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => { vscode.postMessage({ type: "clearTask" }) } - useEffect(() => { - const handleMessage = (e: MessageEvent) => { + const handleMessage = useCallback( + (e: MessageEvent) => { const message: ExtensionMessage = e.data switch (message.type) { case "action": switch (message.action!) { case "didBecomeVisible": - textAreaRef.current?.focus() + if (!isHidden && !textAreaDisabled && !enableButtons) { + textAreaRef.current?.focus() + } break } 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] + ) - window.addEventListener("message", handleMessage) + useEvent("message", handleMessage) + useEffect(() => { const timer = setTimeout(() => { textAreaRef.current?.focus() }, 20) return () => { clearTimeout(timer) - window.removeEventListener("message", handleMessage) } }, []) @@ -271,27 +277,18 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => { if (textAreaRef.current && !textAreaHeight) { setTextAreaHeight(textAreaRef.current.offsetHeight) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [textAreaRef.current]) - - useEffect(() => { - if (!isHidden && !textAreaDisabled && !enableButtons) { - textAreaRef.current?.focus() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isHidden]) + }, [textAreaHeight]) useEffect(() => { const timer = setTimeout(() => { - if (!textAreaDisabled && !enableButtons) { + if (!isHidden && !textAreaDisabled && !enableButtons) { textAreaRef.current?.focus() } }, 50) return () => { clearTimeout(timer) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [textAreaDisabled]) + }, [isHidden, textAreaDisabled, enableButtons]) return (
{ tokensOut={apiMetrics.totalTokensOut} totalCost={apiMetrics.totalCost} onClose={handleTaskCloseButtonClick} + isHidden={isHidden} /> ) : (
diff --git a/webview-ui/src/components/SettingsView.tsx b/webview-ui/src/components/SettingsView.tsx index d3cdfbf..588f927 100644 --- a/webview-ui/src/components/SettingsView.tsx +++ b/webview-ui/src/components/SettingsView.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useState } from "react" -import { VSCodeTextField, VSCodeDivider, VSCodeLink, VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import React, { useState } from "react" +import { useMount } from "react-use" import { vscode } from "../utilities/vscode" type SettingsViewProps = { @@ -47,8 +48,8 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer setMaxRequestsErrorMessage(undefined) } } else { - setMaxRequestsErrorMessage(undefined) - } + setMaxRequestsErrorMessage(undefined) + } } const handleSubmit = () => { @@ -59,11 +60,20 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer } // validate as soon as the component is mounted + /* + useEffect will use stale values of variables if they are not included in the dependency array. so trying to use useEffect with a dependency array of only one value for example will use any other variables' old values. In most cases you don't want this, and should opt to use react-use hooks. + useEffect(() => { + // uses someVar and anotherVar + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [someVar]) + + If we only want to run code once on mount we can use react-use's useEffectOnce or useMount + */ + useMount(() => { validateApiKey(apiKey) validateMaxRequests(maxRequestsPerTask) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }) return (
@@ -153,7 +163,7 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer }}>

This project was made for Anthropic's "Build with Claude June 2024 contest" -
+
https://github.com/saoudrizwan/claude-dev diff --git a/webview-ui/src/components/TaskHeader.tsx b/webview-ui/src/components/TaskHeader.tsx index 8381d43..47048f2 100644 --- a/webview-ui/src/components/TaskHeader.tsx +++ b/webview-ui/src/components/TaskHeader.tsx @@ -1,6 +1,7 @@ -import React, { useState } from "react" -import TextTruncate from "react-text-truncate" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import React, { useEffect, useRef, useState } from "react" +import TextTruncate from "react-text-truncate" +import { useWindowSize } from "react-use" interface TaskHeaderProps { taskText: string @@ -8,10 +9,66 @@ interface TaskHeaderProps { tokensOut: number totalCost: number onClose: () => void + isHidden: boolean } -const TaskHeader: React.FC = ({ taskText, tokensIn, tokensOut, totalCost, onClose }) => { +const TaskHeader: React.FC = ({ taskText, tokensIn, tokensOut, totalCost, onClose, isHidden }) => { const [isExpanded, setIsExpanded] = useState(false) + const [textTruncateKey, setTextTruncateKey] = useState(0) + const textContainerRef = useRef(null) + + /* + When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations. + Sources + - https://usehooks-ts.com/react-hook/use-event-listener + - https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs + - https://github.com/streamich/react-use/blob/master/src/useEvent.ts + - https://stackoverflow.com/questions/55565444/how-to-register-event-with-useeffect-hooks + + Before: + + const updateMaxHeight = useCallback(() => { + if (isExpanded && textContainerRef.current) { + const maxHeight = window.innerHeight * (3 / 5) + textContainerRef.current.style.maxHeight = `${maxHeight}px` + } + }, [isExpanded]) + + useEffect(() => { + updateMaxHeight() + }, [isExpanded, updateMaxHeight]) + + useEffect(() => { + window.removeEventListener("resize", updateMaxHeight) + window.addEventListener("resize", updateMaxHeight) + return () => { + window.removeEventListener("resize", updateMaxHeight) + } + }, [updateMaxHeight]) + + After: + */ + + const { height: windowHeight } = useWindowSize() + + useEffect(() => { + if (isExpanded && textContainerRef.current) { + const maxHeight = windowHeight * (3 / 5) + textContainerRef.current.style.maxHeight = `${maxHeight}px` + } + }, [isExpanded, windowHeight]) + + useEffect(() => { + if (!isHidden) { + /* + There's an issue with TextTruncate where it is hidden when removed from the screen. It uses canvas to measure text width and adjusts the content accordingly. However, when the component is hidden, the canvas is not rendered and the text is not measured. + We can fix this by forcing re-render after navigation by using a key prop that changes when you navigate back to the page. + - https://github.com/ShinyChang/React-Text-Truncate?tab=readme-ov-file#faq + */ + setTextTruncateKey((prev) => prev + 1) + } + }, [isHidden]) + const toggleExpand = () => setIsExpanded(!isExpanded) return ( @@ -40,8 +97,15 @@ const TaskHeader: React.FC = ({ taskText, tokensIn, tokensOut,

-
+
= ({ taskText, tokensIn, tokensOut, )}
-
+
Tokens: - + - {tokensIn.toLocaleString()} - - - {tokensOut.toLocaleString()} + + + {tokensIn.toLocaleString()} +
API Cost: