mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Merge pull request #636 from samhvw8/feat/vite-tailwind
refactor: migrate from CRA to Vite and improve testing
This commit is contained in:
@@ -1,24 +1,56 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
"ecmaVersion": 2021,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"@typescript-eslint/naming-convention": ["warn"],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "import",
|
||||
"format": ["camelCase", "PascalCase"]
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": "off",
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
},
|
||||
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"warn",
|
||||
{
|
||||
"allowExpressions": true,
|
||||
"allowTypedFunctionExpressions": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"warn",
|
||||
{
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": ["off", "always"],
|
||||
"quotes": ["warn", "double", { "avoidEscape": true }],
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-var-requires": "warn",
|
||||
"no-extra-semi": "warn",
|
||||
"prefer-const": "warn",
|
||||
"no-mixed-spaces-and-tabs": "warn",
|
||||
"no-case-declarations": "warn",
|
||||
"no-useless-escape": "warn",
|
||||
"require-yield": "warn",
|
||||
"no-empty": "warn",
|
||||
"no-control-regex": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "warn"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true
|
||||
},
|
||||
"ignorePatterns": ["dist/**", "out/**", "webview-ui/**", "**/*.js"]
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-tests": "tsc -p . --outDir out",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
|
||||
"lint": "eslint src --ext ts --quiet && npm run lint --prefix webview-ui",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"start:webview": "cd webview-ui && npm run start",
|
||||
|
||||
@@ -409,15 +409,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
// then convert it to a uri we can use in the webview.
|
||||
|
||||
// The CSS file from the React build output
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, [
|
||||
"webview-ui",
|
||||
"build",
|
||||
"static",
|
||||
"css",
|
||||
"main.css",
|
||||
])
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
|
||||
// The JS file from the React build output
|
||||
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "js", "main.js"])
|
||||
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.js"])
|
||||
|
||||
// The codicon font from the React build output
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
|
||||
|
||||
40
webview-ui/.eslintrc.json
Normal file
40
webview-ui/.eslintrc.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["react", "@typescript-eslint", "react-hooks"],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"react/display-name": "warn",
|
||||
"no-case-declarations": "warn",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
"react/jsx-key": "warn",
|
||||
"no-extra-semi": "warn",
|
||||
"@typescript-eslint/no-var-requires": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
const { override } = require("customize-cra")
|
||||
|
||||
module.exports = override()
|
||||
|
||||
// Jest configuration override
|
||||
module.exports.jest = function (config) {
|
||||
// Configure reporters
|
||||
config.reporters = [["jest-simple-dot-reporter", {}]]
|
||||
|
||||
// Configure module name mapper for CSS modules
|
||||
config.moduleNameMapper = {
|
||||
...config.moduleNameMapper,
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
}
|
||||
|
||||
// Configure transform ignore patterns for ES modules
|
||||
config.transformIgnorePatterns = [
|
||||
"/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)",
|
||||
]
|
||||
|
||||
return config
|
||||
}
|
||||
12
webview-ui/index.html
Normal file
12
webview-ui/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Roo Code</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
13032
webview-ui/package-lock.json
generated
13032
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,14 +2,15 @@
|
||||
"name": "webview-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "jest",
|
||||
"lint": "eslint src --ext ts,tsx --quiet"
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.101",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"debounce": "^2.1.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -17,50 +18,95 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"react-use": "^17.5.1",
|
||||
"react-virtuoso": "^4.7.13",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rewire": "^7.0.0",
|
||||
"shell-quote": "^1.8.2",
|
||||
"styled-components": "^6.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vscrui": "^0.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
"@tailwindcss/vite": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"build": "node ./scripts/build-react-no-split.js",
|
||||
"test": "react-app-rewired test --watchAll=false",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint src --ext ts,tsx"
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/vscode-webview": "^1.57.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-environment-jsdom": "^27.5.1",
|
||||
"jest-simple-dot-reporter": "^1.0.5",
|
||||
"postcss": "^8.5.1",
|
||||
"ts-jest": "^27.1.5",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^5.4.14"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"setupFilesAfterEnv": [
|
||||
"@testing-library/jest-dom/extend-expect"
|
||||
],
|
||||
"preset": "ts-jest",
|
||||
"reporters": [
|
||||
[
|
||||
"jest-simple-dot-reporter",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
"^vscrui$": "<rootDir>/src/__mocks__/vscrui.ts",
|
||||
"^@vscode/webview-ui-toolkit/react$": "<rootDir>/src/__mocks__/@vscode/webview-ui-toolkit/react.ts"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"tsconfig": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
"src"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
"not op_mini all",
|
||||
"last 2 chrome version",
|
||||
"last 2 firefox version",
|
||||
"last 2 safari version"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/vscode-webview": "^1.57.5",
|
||||
"customize-cra": "^1.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jest-simple-dot-reporter": "^1.0.5",
|
||||
"react-app-rewired": "^2.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* A script that overrides some of the create-react-app build script configurations
|
||||
* in order to disable code splitting/chunking and rename the output build files so
|
||||
* they have no hash. (Reference: https://mtm.dev/disable-code-splitting-create-react-app).
|
||||
*
|
||||
* This is crucial for getting React webview code to run because VS Code expects a
|
||||
* single (consistently named) JavaScript and CSS file when configuring webviews.
|
||||
*/
|
||||
|
||||
const rewire = require("rewire")
|
||||
const defaults = rewire("react-scripts/scripts/build.js")
|
||||
const config = defaults.__get__("config")
|
||||
|
||||
/* Modifying Webpack Configuration for 'shared' dir
|
||||
This section uses Rewire to modify Create React App's webpack configuration without ejecting. Rewire allows us to inject and alter the internal build scripts of CRA at runtime. This allows us to maintain a flexible project structure that keeps shared code outside the webview-ui/src directory, while still adhering to CRA's security model that typically restricts imports to within src/.
|
||||
1. Uses the ModuleScopePlugin to whitelist files from the shared directory, allowing them to be imported despite being outside src/. (see: https://stackoverflow.com/questions/44114436/the-create-react-app-imports-restriction-outside-of-src-directory/58321458#58321458)
|
||||
2. Modifies the TypeScript rule to include the shared directory in compilation. This essentially transpiles and includes the ts files in shared dir in the output main.js file.
|
||||
Before, we would just import types from shared dir and specifying include (and alias to have cleaner paths) in tsconfig.json was enough. But now that we are creating values (i.e. models in api.ts) to import into the react app, we must also include these files in the webpack resolution.
|
||||
- Imports from the shared directory must use full paths relative to the src directory, without file extensions.
|
||||
- Example: import { someFunction } from '../../src/shared/utils/helpers'
|
||||
*/
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
// Get all files in the shared directory
|
||||
const sharedDir = path.resolve(__dirname, "..", "..", "src", "shared")
|
||||
|
||||
function getAllFiles(dir) {
|
||||
let files = []
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
const filePath = path.join(dir, file)
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
files = files.concat(getAllFiles(filePath))
|
||||
} else {
|
||||
const withoutExtension = path.join(dir, path.parse(file).name)
|
||||
files.push(withoutExtension)
|
||||
}
|
||||
})
|
||||
return files
|
||||
}
|
||||
const sharedFiles = getAllFiles(sharedDir)
|
||||
// config.resolve.plugins = config.resolve.plugins.filter((plugin) => !(plugin instanceof ModuleScopePlugin))
|
||||
// Instead of excluding the whole ModuleScopePlugin, we just whitelist specific files that can be imported from outside src.
|
||||
config.resolve.plugins.forEach((plugin) => {
|
||||
if (plugin instanceof ModuleScopePlugin) {
|
||||
console.log("Whitelisting shared files: ", sharedFiles)
|
||||
sharedFiles.forEach((file) => plugin.allowedFiles.add(file))
|
||||
}
|
||||
})
|
||||
/*
|
||||
Webpack configuration
|
||||
|
||||
Webpack is a module bundler for JavaScript applications. It processes your project files, resolving dependencies and generating a deployable production build.
|
||||
The webpack config is an object that tells webpack how to process and bundle your code. It defines entry points, output settings, and how to handle different file types.
|
||||
This config.module section of the webpack config deals with how different file types (modules) should be treated.
|
||||
config.module.rules:
|
||||
Rules define how module files should be processed. Each rule can:
|
||||
- Specify which files to process (test)
|
||||
When webpack "processes" a file, it performs several operations:
|
||||
1. Reads the file
|
||||
2. Parses its content and analyzes dependencies
|
||||
3. Applies transformations (e.g., converting TypeScript to JavaScript)
|
||||
4. Potentially modifies the code (e.g., applying polyfills)
|
||||
5. Includes the processed file in the final bundle
|
||||
By specifying which files to process, we're telling webpack which files should go through this pipeline and be included in our application bundle. Files that aren't processed are ignored by webpack.
|
||||
In our case, we're ensuring that TypeScript files in our shared directory are processed, allowing us to use them in our application.
|
||||
- Define which folders to include or exclude
|
||||
- Set which loaders to use for transformation
|
||||
A loader transforms certain types of files into valid modules that webpack can process. For example, the TypeScript loader converts .ts files into JavaScript that webpack can understand.
|
||||
By modifying these rules, we can change how webpack processes different files in our project, allowing us to include files from outside the standard src directory.
|
||||
|
||||
Why we need to modify the webpack config
|
||||
|
||||
Create React App (CRA) is designed to only process files within the src directory for security reasons. (CRA limits processing to the src directory to prevent accidental inclusion of sensitive files, reduce the attack surface, and ensure predictable builds, enhancing overall project security and consistency. Therefore it's essential that if you do include files outside src, you do so explicitly.)
|
||||
To use files from the shared directory, we need to:
|
||||
1. Modify ModuleScopePlugin to allow imports from the shared directory.
|
||||
2. Update the TypeScript loader rule to process TypeScript files from the shared directory.
|
||||
These changes tell webpack it's okay to import from the shared directory and ensure that TypeScript files in this directory are properly converted to JavaScript.
|
||||
|
||||
Modify webpack configuration to process TypeScript files from shared directory
|
||||
|
||||
This code modifies the webpack configuration to allow processing of TypeScript files from our shared directory, which is outside the standard src folder.
|
||||
1. config.module.rules[1]: In Create React App's webpack config, the second rule (index 1) typically contains the rules for processing JavaScript and TypeScript files.
|
||||
2. .oneOf: This array contains a list of loaders, and webpack will use the first matching loader for each file. We iterate through these to find the TypeScript loader.
|
||||
3. We check each rule to see if it applies to TypeScript files by looking for 'ts|tsx' in the test regex.
|
||||
4. When we find the TypeScript rule, we add our shared directory to its 'include' array. This tells webpack to also process TypeScript files from the shared directory.
|
||||
Note: This code assumes a specific structure in the CRA webpack config. If CRA updates its config structure in future versions, this code might need to be adjusted.
|
||||
*/
|
||||
config.module.rules[1].oneOf.forEach((rule) => {
|
||||
if (rule.test && rule.test.toString().includes("ts|tsx")) {
|
||||
// rule.include is path to src by default, but we can update rule.include to be an array as it matches an expected schema by react-scripts
|
||||
rule.include = [rule.include, sharedDir].filter(Boolean)
|
||||
}
|
||||
})
|
||||
|
||||
// Disable code splitting
|
||||
config.optimization.splitChunks = {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Disable code chunks
|
||||
config.optimization.runtimeChunk = false
|
||||
|
||||
// Rename main.{hash}.js to main.js
|
||||
config.output.filename = "static/js/[name].js"
|
||||
|
||||
// Rename main.{hash}.css to main.css
|
||||
config.plugins[5].options.filename = "static/css/[name].css"
|
||||
config.plugins[5].options.moduleFilename = () => "static/css/main.css"
|
||||
117
webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.ts
Normal file
117
webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import React from "react"
|
||||
|
||||
interface VSCodeProps {
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
onChange?: (e: any) => void
|
||||
onInput?: (e: any) => void
|
||||
appearance?: string
|
||||
checked?: boolean
|
||||
value?: string | number
|
||||
placeholder?: string
|
||||
href?: string
|
||||
"data-testid"?: string
|
||||
style?: React.CSSProperties
|
||||
slot?: string
|
||||
role?: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export const VSCodeButton: React.FC<VSCodeProps> = ({ children, onClick, appearance, className, ...props }) => {
|
||||
// For icon buttons, render children directly without any wrapping
|
||||
if (appearance === "icon") {
|
||||
return React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick,
|
||||
className: `${className || ""}`,
|
||||
"data-appearance": appearance,
|
||||
...props,
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
// For regular buttons
|
||||
return React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick,
|
||||
className: className,
|
||||
...props,
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
export const VSCodeCheckbox: React.FC<VSCodeProps> = ({ children, onChange, checked, ...props }) =>
|
||||
React.createElement("label", {}, [
|
||||
React.createElement("input", {
|
||||
key: "input",
|
||||
type: "checkbox",
|
||||
checked,
|
||||
onChange: (e: any) => onChange?.({ target: { checked: e.target.checked } }),
|
||||
"aria-label": typeof children === "string" ? children : undefined,
|
||||
...props,
|
||||
}),
|
||||
children && React.createElement("span", { key: "label" }, children),
|
||||
])
|
||||
|
||||
export const VSCodeTextField: React.FC<VSCodeProps> = ({ children, value, onInput, placeholder, ...props }) =>
|
||||
React.createElement("div", { style: { position: "relative", display: "inline-block", width: "100%" } }, [
|
||||
React.createElement("input", {
|
||||
key: "input",
|
||||
type: "text",
|
||||
value,
|
||||
onChange: (e: any) => onInput?.({ target: { value: e.target.value } }),
|
||||
placeholder,
|
||||
...props,
|
||||
}),
|
||||
children,
|
||||
])
|
||||
|
||||
export const VSCodeTextArea: React.FC<VSCodeProps> = ({ value, onChange, ...props }) =>
|
||||
React.createElement("textarea", {
|
||||
value,
|
||||
onChange: (e: any) => onChange?.({ target: { value: e.target.value } }),
|
||||
...props,
|
||||
})
|
||||
|
||||
export const VSCodeLink: React.FC<VSCodeProps> = ({ children, href, ...props }) =>
|
||||
React.createElement("a", { href: href || "#", ...props }, children)
|
||||
|
||||
export const VSCodeDropdown: React.FC<VSCodeProps> = ({ children, value, onChange, ...props }) =>
|
||||
React.createElement("select", { value, onChange, ...props }, children)
|
||||
|
||||
export const VSCodeOption: React.FC<VSCodeProps> = ({ children, value, ...props }) =>
|
||||
React.createElement("option", { value, ...props }, children)
|
||||
|
||||
export const VSCodeRadio: React.FC<VSCodeProps> = ({ children, value, checked, onChange, ...props }) =>
|
||||
React.createElement("label", { style: { display: "inline-flex", alignItems: "center" } }, [
|
||||
React.createElement("input", {
|
||||
key: "input",
|
||||
type: "radio",
|
||||
value,
|
||||
checked,
|
||||
onChange,
|
||||
...props,
|
||||
}),
|
||||
children && React.createElement("span", { key: "label", style: { marginLeft: "4px" } }, children),
|
||||
])
|
||||
|
||||
export const VSCodeRadioGroup: React.FC<VSCodeProps> = ({ children, onChange, ...props }) =>
|
||||
React.createElement("div", { role: "radiogroup", onChange, ...props }, children)
|
||||
|
||||
export const VSCodeSlider: React.FC<VSCodeProps> = ({ value, onChange, ...props }) =>
|
||||
React.createElement("input", {
|
||||
type: "range",
|
||||
value,
|
||||
onChange: (e: any) => onChange?.({ target: { value: Number(e.target.value) } }),
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
style: { flexGrow: 1, height: "2px" },
|
||||
...props,
|
||||
})
|
||||
14
webview-ui/src/__mocks__/vscrui.ts
Normal file
14
webview-ui/src/__mocks__/vscrui.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react"
|
||||
|
||||
export const Checkbox = ({ children, checked, onChange }: any) =>
|
||||
React.createElement("div", { "data-testid": "mock-checkbox", onClick: onChange }, children)
|
||||
|
||||
export const Dropdown = ({ children, value, onChange }: any) =>
|
||||
React.createElement("div", { "data-testid": "mock-dropdown", onClick: onChange }, children)
|
||||
|
||||
export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children)
|
||||
|
||||
export type DropdownOption = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
@@ -37,9 +37,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
const isLastApiReqInterrupted = useMemo(() => {
|
||||
// Check if last api_req_started is cancelled
|
||||
const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started")
|
||||
if (lastApiReqStarted?.text != null) {
|
||||
const info = JSON.parse(lastApiReqStarted.text)
|
||||
if (info.cancelReason != null) {
|
||||
if (lastApiReqStarted?.text) {
|
||||
const info = JSON.parse(lastApiReqStarted.text) as { cancelReason: string | null }
|
||||
if (info && info.cancelReason !== null) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { render, fireEvent, screen } from "@testing-library/react"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import AutoApproveMenu from "../AutoApproveMenu"
|
||||
import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes"
|
||||
import { experimentDefault } from "../../../../../src/shared/experiments"
|
||||
|
||||
// Mock the ExtensionStateContext hook
|
||||
jest.mock("../../../context/ExtensionStateContext")
|
||||
@@ -41,6 +42,8 @@ describe("AutoApproveMenu", () => {
|
||||
openAiModels: [],
|
||||
mcpServers: [],
|
||||
filePaths: [],
|
||||
experiments: experimentDefault,
|
||||
customModes: [],
|
||||
|
||||
// Auto-approve specific properties
|
||||
alwaysAllowReadOnly: false,
|
||||
@@ -49,6 +52,7 @@ describe("AutoApproveMenu", () => {
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysApproveResubmit: false,
|
||||
alwaysAllowModeSwitch: false,
|
||||
autoApprovalEnabled: false,
|
||||
|
||||
// Required setter functions
|
||||
@@ -59,6 +63,7 @@ describe("AutoApproveMenu", () => {
|
||||
setAlwaysAllowExecute: jest.fn(),
|
||||
setAlwaysAllowBrowser: jest.fn(),
|
||||
setAlwaysAllowMcp: jest.fn(),
|
||||
setAlwaysAllowModeSwitch: jest.fn(),
|
||||
setShowAnnouncement: jest.fn(),
|
||||
setAllowedCommands: jest.fn(),
|
||||
setSoundEnabled: jest.fn(),
|
||||
@@ -77,9 +82,13 @@ describe("AutoApproveMenu", () => {
|
||||
setListApiConfigMeta: jest.fn(),
|
||||
onUpdateApiConfig: jest.fn(),
|
||||
setMode: jest.fn(),
|
||||
setCustomPrompts: jest.fn(),
|
||||
setCustomModePrompts: jest.fn(),
|
||||
setCustomSupportPrompts: jest.fn(),
|
||||
setEnhancementApiConfigId: jest.fn(),
|
||||
setAutoApprovalEnabled: jest.fn(),
|
||||
setExperimentEnabled: jest.fn(),
|
||||
handleInputChange: jest.fn(),
|
||||
setCustomModes: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -16,7 +16,6 @@ jest.mock("../../../components/common/MarkdownBlock")
|
||||
|
||||
// Get the mocked postMessage function
|
||||
const mockPostMessage = vscode.postMessage as jest.Mock
|
||||
/* eslint-enable import/first */
|
||||
|
||||
// Mock ExtensionStateContext
|
||||
jest.mock("../../../context/ExtensionStateContext")
|
||||
|
||||
@@ -202,6 +202,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
<VSCodeRadioGroup
|
||||
style={{ display: "flex", flexWrap: "wrap" }}
|
||||
value={sortOption}
|
||||
role="radiogroup"
|
||||
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
|
||||
<VSCodeRadio value="newest">Newest</VSCodeRadio>
|
||||
<VSCodeRadio value="oldest">Oldest</VSCodeRadio>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import PromptsView from "../PromptsView"
|
||||
import { ExtensionStateContext } from "../../../context/ExtensionStateContext"
|
||||
@@ -98,20 +98,14 @@ describe("PromptsView", () => {
|
||||
expect(codeTab).toHaveAttribute("data-active", "false")
|
||||
})
|
||||
|
||||
it("handles prompt changes correctly", () => {
|
||||
it("handles prompt changes correctly", async () => {
|
||||
renderPromptsView()
|
||||
|
||||
const textarea = screen.getByTestId("code-prompt-textarea")
|
||||
fireEvent(
|
||||
textarea,
|
||||
new CustomEvent("change", {
|
||||
detail: {
|
||||
target: {
|
||||
value: "New prompt value",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Get the textarea
|
||||
const textarea = await waitFor(() => screen.getByTestId("code-prompt-textarea"))
|
||||
fireEvent.change(textarea, {
|
||||
target: { value: "New prompt value" },
|
||||
})
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "updatePrompt",
|
||||
@@ -163,24 +157,18 @@ describe("PromptsView", () => {
|
||||
expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("handles API configuration selection", () => {
|
||||
it("handles API configuration selection", async () => {
|
||||
renderPromptsView()
|
||||
|
||||
// Click the ENHANCE tab first to show the API config dropdown
|
||||
const enhanceTab = screen.getByTestId("ENHANCE-tab")
|
||||
fireEvent.click(enhanceTab)
|
||||
|
||||
const dropdown = screen.getByTestId("api-config-dropdown")
|
||||
fireEvent(
|
||||
dropdown,
|
||||
new CustomEvent("change", {
|
||||
detail: {
|
||||
target: {
|
||||
value: "config1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Wait for the ENHANCE tab click to take effect
|
||||
const dropdown = await waitFor(() => screen.getByTestId("api-config-dropdown"))
|
||||
fireEvent.change(dropdown, {
|
||||
target: { value: "config1" },
|
||||
})
|
||||
|
||||
expect(mockExtensionState.setEnhancementApiConfigId).toHaveBeenCalledWith("config1")
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
@@ -198,13 +186,9 @@ describe("PromptsView", () => {
|
||||
})
|
||||
|
||||
const textarea = screen.getByTestId("global-custom-instructions-textarea")
|
||||
const changeEvent = new CustomEvent("change", {
|
||||
detail: { target: { value: "" } },
|
||||
fireEvent.change(textarea, {
|
||||
target: { value: "" },
|
||||
})
|
||||
Object.defineProperty(changeEvent, "target", {
|
||||
value: { value: "" },
|
||||
})
|
||||
await fireEvent(textarea, changeEvent)
|
||||
|
||||
expect(setCustomInstructions).toHaveBeenCalledWith(undefined)
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
|
||||
@@ -41,7 +41,10 @@ describe("ApiConfigManager", () => {
|
||||
|
||||
const defaultProps = {
|
||||
currentApiConfigName: "Default Config",
|
||||
listApiConfigMeta: [{ name: "Default Config" }, { name: "Another Config" }],
|
||||
listApiConfigMeta: [
|
||||
{ id: "default", name: "Default Config" },
|
||||
{ id: "another", name: "Another Config" },
|
||||
],
|
||||
onSelectConfig: mockOnSelectConfig,
|
||||
onDeleteConfig: mockOnDeleteConfig,
|
||||
onRenameConfig: mockOnRenameConfig,
|
||||
@@ -120,7 +123,7 @@ describe("ApiConfigManager", () => {
|
||||
})
|
||||
|
||||
it("disables delete button when only one config exists", () => {
|
||||
render(<ApiConfigManager {...defaultProps} listApiConfigMeta={[{ name: "Default Config" }]} />)
|
||||
render(<ApiConfigManager {...defaultProps} listApiConfigMeta={[{ id: "default", name: "Default Config" }]} />)
|
||||
|
||||
const deleteButton = screen.getByTitle("Cannot delete the only profile")
|
||||
expect(deleteButton).toHaveAttribute("disabled")
|
||||
|
||||
@@ -1,3 +1,67 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
/* Theme Variables - VSCode Integration */
|
||||
:root {
|
||||
/* Base Colors */
|
||||
--background: var(--vscode-editor-background);
|
||||
--foreground: var(--vscode-editor-foreground);
|
||||
|
||||
/* Component Colors */
|
||||
--card: var(--vscode-editor-background);
|
||||
--card-foreground: var(--vscode-editor-foreground);
|
||||
--popover: var(--vscode-menu-background, var(--vscode-editor-background));
|
||||
--popover-foreground: var(--vscode-menu-foreground, var(--vscode-editor-foreground));
|
||||
|
||||
/* Button Colors */
|
||||
--primary: var(--vscode-button-background);
|
||||
--primary-foreground: var(--vscode-button-foreground);
|
||||
--secondary: var(--vscode-button-secondaryBackground);
|
||||
--secondary-foreground: var(--vscode-button-secondaryForeground);
|
||||
--accent: var(--vscode-focusBorder);
|
||||
--accent-foreground: var(--vscode-button-foreground);
|
||||
|
||||
/* State Colors */
|
||||
--muted: var(--vscode-disabledForeground);
|
||||
--muted-foreground: var(--vscode-descriptionForeground);
|
||||
--destructive: var(--vscode-errorForeground);
|
||||
--destructive-foreground: var(--vscode-editor-background);
|
||||
|
||||
/* UI Elements */
|
||||
--border: var(--vscode-widget-border);
|
||||
--input: var(--vscode-input-background);
|
||||
--ring: var(--vscode-focusBorder);
|
||||
--radius: 0.5rem;
|
||||
|
||||
/* Chart Colors - Using VSCode's chart colors */
|
||||
--chart-1: var(--vscode-charts-red);
|
||||
--chart-2: var(--vscode-charts-blue);
|
||||
--chart-3: var(--vscode-charts-yellow);
|
||||
--chart-4: var(--vscode-charts-orange);
|
||||
--chart-5: var(--vscode-charts-green);
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Border Styles */
|
||||
.border,
|
||||
.border-r,
|
||||
.border-l,
|
||||
.border-t,
|
||||
.border-b,
|
||||
.border-x,
|
||||
.border-y {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* Code Block Styles */
|
||||
pre,
|
||||
code {
|
||||
background-color: var(--vscode-textCodeBlock-background);
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Element Focus States */
|
||||
textarea:focus {
|
||||
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import "./index.css"
|
||||
import App from "./App"
|
||||
import reportWebVitals from "./reportWebVitals"
|
||||
import "../../node_modules/@vscode/codicons/dist/codicon.css"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
@@ -11,8 +10,3 @@ root.render(
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals()
|
||||
|
||||
1
webview-ui/src/react-app-env.d.ts
vendored
1
webview-ui/src/react-app-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ReportHandler } from "web-vitals"
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default reportWebVitals
|
||||
23
webview-ui/vite.config.mts
Normal file
23
webview-ui/vite.config.mts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
build: {
|
||||
outDir: "build",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user