mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
fix: update with fixes from new version
This commit is contained in:
commit
33f2532ae8
7
.github/workflows/build-helper.yml
vendored
7
.github/workflows/build-helper.yml
vendored
@ -16,6 +16,10 @@ jobs:
|
||||
arch: "amd64"
|
||||
runner: "ubuntu-latest"
|
||||
scripthaus: "build-package-linux"
|
||||
- platform: "linux"
|
||||
arch: "arm64"
|
||||
runner: ubuntu-24.04-arm64-16core
|
||||
scripthaus: "build-package-linux"
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -28,6 +32,8 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm
|
||||
- name: Install FPM # The version of FPM that comes bundled with electron-builder doesn't include a Linux ARM target. Installing Gems onto the runner is super quick so we'll just do this for all targets.
|
||||
run: sudo gem install fpm
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
@ -58,6 +64,7 @@ jobs:
|
||||
run: scripthaus run ${{ matrix.scripthaus }}
|
||||
env:
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
|
||||
CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE}}
|
||||
CSC_KEY_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE_PWD }}
|
||||
APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
||||
|
85
.github/workflows/regression.yml
vendored
85
.github/workflows/regression.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: TestDriver.ai Regression Testing
|
||||
name: TestDriver.ai Regression Testing - Waveterm
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@ -14,53 +14,38 @@ permissions:
|
||||
contents: read # To allow the action to read repository contents
|
||||
pull-requests: write # To allow the action to create/update pull request comments
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: TestDriver
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dashcamio/testdriver@main
|
||||
id: testdriver
|
||||
with:
|
||||
version: v2.12.12
|
||||
prerun: |
|
||||
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
|
||||
cd ~/actions-runner/_work/testdriver/testdriver/
|
||||
brew install go
|
||||
brew tap scripthaus-dev/scripthaus
|
||||
brew install corepack
|
||||
brew install scripthaus
|
||||
corepack enable
|
||||
yarn install
|
||||
scripthaus run build-backend
|
||||
echo "Yarn"
|
||||
yarn
|
||||
echo "Rebuild"
|
||||
scripthaus run electron-rebuild
|
||||
echo "Webpack"
|
||||
scripthaus run webpack-build
|
||||
echo "Starting Electron"
|
||||
scripthaus run electron 1>/dev/null 2>&1 &
|
||||
echo "Electron Done"
|
||||
exit
|
||||
prompt: |
|
||||
1. wait 10 seconds
|
||||
1. click "Get Started"
|
||||
1. validate that overlapping text does not appear in the application
|
||||
1. focus the Wave input with the keyboard shorcut Command + I
|
||||
1. type 'ls' into the input
|
||||
1. press return
|
||||
1. validate Wave shows the result of 'ls'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: peter-evans/create-or-update-comment@v4
|
||||
if: ${{always()}}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
## TestDriver Summary
|
||||
${{ steps.testdriver.outputs.markdown }}
|
||||
${{ steps.testdriver.outputs.summary }}
|
||||
reactions: |
|
||||
+1
|
||||
-1
|
||||
test:
|
||||
name: "TestDriver"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dashcamio/testdriver@main
|
||||
id: testdriver
|
||||
with:
|
||||
version: v3.9.0
|
||||
key: ${{secrets.DASHCAM_API}}
|
||||
os: mac
|
||||
prerun: |
|
||||
cd ~/actions-runner/_work/testdriver/testdriver/
|
||||
brew install go
|
||||
brew tap scripthaus-dev/scripthaus
|
||||
brew install corepack
|
||||
brew install scripthaus
|
||||
corepack enable
|
||||
yarn install
|
||||
scripthaus run build-backend
|
||||
echo "Yarn"
|
||||
yarn
|
||||
echo "Rebuild"
|
||||
scripthaus run electron-rebuild
|
||||
echo "Webpack"
|
||||
scripthaus run webpack-build
|
||||
echo "Starting Electron"
|
||||
scripthaus run electron 1>/dev/null 2>&1 &
|
||||
echo "Electron Done"
|
||||
cd /Users/ec2-user/Downloads/td/
|
||||
npm rebuild
|
||||
exit
|
||||
prompt: |
|
||||
1. /run /Users/ec2-user/actions-runner/_work/testdriver/testdriver/.testdriver/wave1.yml
|
||||
|
@ -1,18 +0,0 @@
|
||||
rm ~/Desktop/WITH-LOVE-FROM-AMERICA.txt
|
||||
cd ~/actions-runner/_work/testdriver/testdriver/
|
||||
brew install go
|
||||
brew tap scripthaus-dev/scripthaus
|
||||
brew install scripthaus
|
||||
corepack enable
|
||||
yarn install
|
||||
scripthaus run build-backend
|
||||
echo "Yarn"
|
||||
yarn
|
||||
echo "Rebuild"
|
||||
scripthaus run electron-rebuild
|
||||
echo "Webpack"
|
||||
scripthaus run webpack-build
|
||||
echo "Starting Electron"
|
||||
scripthaus run electron 1>/dev/null 2>&1 &
|
||||
echo "Electron Done"
|
||||
exit
|
37
.testdriver/wave1.yml
Normal file
37
.testdriver/wave1.yml
Normal file
@ -0,0 +1,37 @@
|
||||
version: 3.8.0
|
||||
steps:
|
||||
- prompt: "Focus electron"
|
||||
commands:
|
||||
- command: focus-application
|
||||
name: Electron
|
||||
- command: hover-text
|
||||
description: Get started CTA
|
||||
text: Get Started
|
||||
action: click
|
||||
- command: hover-text
|
||||
description: Settings button
|
||||
text: Settings
|
||||
action: click
|
||||
- command: hover-text
|
||||
description: font size 13
|
||||
text: 13px
|
||||
action: click
|
||||
- command: hover-text
|
||||
description: font size 12
|
||||
text: 12px
|
||||
action: click
|
||||
- command: hover-text
|
||||
description: theme selector
|
||||
text: Dark
|
||||
action: click
|
||||
- command: hover-text
|
||||
description: theme color white
|
||||
text: Light
|
||||
action: click
|
||||
- command: hover-text
|
||||
description: workspace
|
||||
text: workspace-1
|
||||
action: click
|
||||
- command: assert
|
||||
expect: the terminal is white
|
||||
|
@ -309,10 +309,6 @@
|
||||
"command": "aichat:clearHistory",
|
||||
"keys": ["Ctrl:l"]
|
||||
},
|
||||
{
|
||||
"command": "aichat:setCmdInputValue",
|
||||
"keys": ["Ctrl:Shift:e"]
|
||||
},
|
||||
{
|
||||
"command": "terminal:copy",
|
||||
"keys": ["Ctrl:Shift:c"]
|
||||
|
@ -34,20 +34,23 @@ You'll now have to move the built `scripthaus` binary to a directory in your pat
|
||||
sudo cp scripthaus /usr/local/bin
|
||||
```
|
||||
|
||||
## Install nodejs, npm, and yarn
|
||||
## Install nodejs and yarn
|
||||
|
||||
We use [nvm](https://github.com/nvm-sh/nvm) to install nodejs on Linux (you can use an alternate installer if you wish). You must have a relatively recent version of node in order to build the terminal. Different distributions and shells will require different setup instructions. These instructions work for Ubuntu 22 using bash (will install node v20.8.1):
|
||||
You also need a relatively modern nodejs with npm and yarn installed.
|
||||
|
||||
```
|
||||
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
|
||||
source ~/.bashrc
|
||||
nvm install v20.8.1
|
||||
```
|
||||
Node can be installed from [https://nodejs.org](https://nodejs.org).
|
||||
|
||||
Now we can install yarn:
|
||||
We use Yarn Modern to manage our packages. The recommended way to install Yarn Modern is using Corepack, a new utility shipped by NodeJS that lets you manage your package manager versioning as you would any packages.
|
||||
|
||||
```
|
||||
npm install -g yarn
|
||||
If you installed NodeJS from the official feed (via the website or using NVM), this should come preinstalled. If you use Homebrew or some other feed, you may need to manually install Corepack using `npm install -g corepack`.
|
||||
|
||||
For more information on Corepack, check out [this link](https://yarnpkg.com/corepack).
|
||||
|
||||
Once you've verified that you have Corepack installed, run the following script to set up Yarn for the repository:
|
||||
|
||||
```sh
|
||||
corepack enable
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Clone the Wave Repo
|
||||
|
104
package.json
104
package.json
@ -18,95 +18,95 @@
|
||||
"appId": "dev.commandline.waveterm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.14.3",
|
||||
"@monaco-editor/react": "^4.5.1",
|
||||
"@lexical/react": "^0.17.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@table-nav/core": "^0.0.7",
|
||||
"@table-nav/react": "^0.0.7",
|
||||
"@tanstack/match-sorter-utils": "^8.8.4",
|
||||
"@tanstack/react-table": "^8.10.3",
|
||||
"@withfig/autocomplete": "^2.652.3",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-table": "^8.20.1",
|
||||
"@withfig/autocomplete": "^2.672.0",
|
||||
"autobind-decorator": "^2.4.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.3",
|
||||
"dompurify": "^3.0.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-updater": "^6.1.8",
|
||||
"framer-motion": "^10.16.16",
|
||||
"dayjs": "^1.11.12",
|
||||
"dompurify": "^3.1.6",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "^6.3.2",
|
||||
"framer-motion": "^10.18.0",
|
||||
"lexical": "0.14.5",
|
||||
"mobx": "6.12",
|
||||
"mobx-react": "^7.5.0",
|
||||
"mobx": "6.12.5",
|
||||
"mobx-react": "^7.6.0",
|
||||
"monaco-editor": "0.48.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"overlayscrollbars": "^2.6.1",
|
||||
"overlayscrollbars-react": "^0.5.5",
|
||||
"node-fetch": "^3.3.2",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sprintf-js": "^1.1.2",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"sprintf-js": "^1.1.3",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"tsx-control-statements": "^5.1.1",
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.8.2",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.13.1",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-serialize": "^0.11.0",
|
||||
"xterm-addon-web-links": "^0.9.0",
|
||||
"xterm-addon-webgl": "^0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.17.10",
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/cli": "^7.24.8",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.18.2",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/plugin-transform-react-jsx": "^7.17.12",
|
||||
"@babel/plugin-transform-runtime": "^7.23.4",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@babel/plugin-transform-react-jsx": "^7.25.2",
|
||||
"@babel/plugin-transform-runtime": "^7.24.7",
|
||||
"@babel/preset-env": "^7.25.3",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/papaparse": "^5.3.10",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/sprintf-js": "^1.1.3",
|
||||
"@types/throttle-debounce": "^5.0.1",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/webpack-env": "^1.18.3",
|
||||
"@withfig/autocomplete-types": "^1.30.0",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/sprintf-js": "^1.1.4",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"@withfig/autocomplete-types": "^1.31.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-plugin-jsx-control-statements": "^4.1.2",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"css-loader": "^7.1.0",
|
||||
"electron": "^30.0.8",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"css-loader": "^7.1.2",
|
||||
"electron": "^31.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-builder-squirrel-windows": "^24.13.3",
|
||||
"electron-builder-squirrel-windows": "25.0.0-alpha.10",
|
||||
"file-loader": "^6.2.0",
|
||||
"http-server": "^14.1.1",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^12.0.0",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"lodash-webpack-plugin": "^0.11.6",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"prettier": "^2.8.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-split-it": "^2.0.0",
|
||||
"style-loader": "4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"yaml": "^2.4.0"
|
||||
"webpack-merge": "^5.10.0",
|
||||
"yaml": "^2.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
|
@ -170,6 +170,7 @@
|
||||
--cmdinput-button-bg-color: var(--tab-green);
|
||||
--cmdinput-disabled-bg-color: var(--app-text-bg-disabled-color);
|
||||
--cmdinput-history-bg-color: var(--app-bg-color);
|
||||
--cmdinput-ghost-text-color: rgb(145, 150, 144);
|
||||
|
||||
/* screen view color */
|
||||
--screen-view-text-caption-color: rgb(139, 145, 138);
|
||||
|
@ -50,6 +50,7 @@
|
||||
--modal-header-bottom-border-color: rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* cmd input */
|
||||
--cmdinput-ghost-text-color: rgb(116, 116, 116);
|
||||
|
||||
/* scroll colors */
|
||||
--scrollbar-background-color: var(--app-bg-color);
|
||||
|
@ -7,7 +7,6 @@ export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
|
||||
export { InputDecoration } from "./inputdecoration";
|
||||
export { LinkButton } from "./linkbutton";
|
||||
export { Markdown } from "./markdown";
|
||||
export { Markdown2 } from "./markdown2";
|
||||
export { Modal } from "./modal";
|
||||
export { PasswordField } from "./passwordfield";
|
||||
export { ResizableSidebar } from "./resizablesidebar";
|
||||
|
@ -46,7 +46,7 @@
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
|
||||
pre.codeblock {
|
||||
pre {
|
||||
background-color: var(--markdown-bg-color);
|
||||
margin: 4px 10px;
|
||||
padding: 0.4em 0.7em;
|
||||
|
@ -5,14 +5,14 @@ import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CopyButton } from "@/elements";
|
||||
import { clsx } from "clsx";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as mobx from "mobx";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
|
||||
import "./markdown.less";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
function LinkRenderer(props: any): any {
|
||||
function Link(props: any): JSX.Element {
|
||||
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
||||
return (
|
||||
<a href={newUrl} target="_blank" rel={"noopener"}>
|
||||
@ -21,99 +21,86 @@ function LinkRenderer(props: any): any {
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderRenderer(props: any, hnum: number): any {
|
||||
function Header(props: any, hnum: number): JSX.Element {
|
||||
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
|
||||
}
|
||||
|
||||
function CodeRenderer(props: any): any {
|
||||
function Code(props: any): JSX.Element {
|
||||
return <code>{props.children}</code>;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class CodeBlockMarkdown extends React.Component<
|
||||
{ children: React.ReactNode; codeSelectSelectedIndex?: number; uuid: string },
|
||||
{}
|
||||
> {
|
||||
blockIndex: number;
|
||||
blockRef: React.RefObject<HTMLPreElement>;
|
||||
const CodeBlock = mobxReact.observer(
|
||||
(props: { children: React.ReactNode; onClickExecute?: (cmd: string) => void }): JSX.Element => {
|
||||
const copied: OV<boolean> = mobx.observable.box(false, { name: "copied" });
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.blockRef = React.createRef();
|
||||
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef, this.props.uuid);
|
||||
}
|
||||
const getTextContent = (children: any) => {
|
||||
if (typeof children === "string") {
|
||||
return children;
|
||||
} else if (Array.isArray(children)) {
|
||||
return children.map(getTextContent).join("");
|
||||
} else if (children.props && children.props.children) {
|
||||
return getTextContent(children.props.children);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
render() {
|
||||
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void;
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
|
||||
const sel = window.getSelection();
|
||||
if (sel?.toString().length == 0) {
|
||||
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
let textToCopy = getTextContent(props.children);
|
||||
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
copied.set(true);
|
||||
setTimeout(() => copied.set(false), 2000); // Reset copied state after 2 seconds
|
||||
};
|
||||
|
||||
const handleExecute = (e: React.MouseEvent) => {
|
||||
let textToCopy = getTextContent(props.children);
|
||||
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
|
||||
if (props.onClickExecute) {
|
||||
props.onClickExecute(textToCopy);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
|
||||
|
||||
return (
|
||||
<pre
|
||||
ref={this.blockRef}
|
||||
className={clsx({ selected: selected })}
|
||||
onClick={(event) => clickHandler(event, this.blockIndex)}
|
||||
>
|
||||
{this.props.children}
|
||||
<pre className="codeblock">
|
||||
{props.children}
|
||||
<div className="codeblock-actions">
|
||||
<CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
|
||||
<If condition={props.onClickExecute}>
|
||||
<i className="fa-regular fa-square-terminal" onClick={handleExecute}></i>
|
||||
</If>
|
||||
</div>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@mobxReact.observer
|
||||
class Markdown extends React.Component<
|
||||
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
|
||||
{
|
||||
text: string;
|
||||
style?: any;
|
||||
className?: string;
|
||||
onClickExecute?: (cmd: string) => void;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
curUuid: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.curUuid = uuidv4();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number, curUuid: string): any {
|
||||
if (codeSelect) {
|
||||
return (
|
||||
<CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex} uuid={curUuid}>
|
||||
{props.children}
|
||||
</CodeBlockMarkdown>
|
||||
);
|
||||
} else {
|
||||
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
||||
let blockText = (e.target as HTMLElement).innerText;
|
||||
if (blockText) {
|
||||
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
|
||||
navigator.clipboard.writeText(blockText);
|
||||
}
|
||||
};
|
||||
return <pre onClick={(event) => clickHandler(event)}>{props.children}</pre>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let text = this.props.text;
|
||||
let codeSelect = this.props.codeSelect;
|
||||
let curCodeSelectIndex = GlobalModel.inputModel.getCodeSelectSelectedIndex();
|
||||
let { text, className, onClickExecute } = this.props;
|
||||
let markdownComponents = {
|
||||
a: LinkRenderer,
|
||||
h1: (props) => HeaderRenderer(props, 1),
|
||||
h2: (props) => HeaderRenderer(props, 2),
|
||||
h3: (props) => HeaderRenderer(props, 3),
|
||||
h4: (props) => HeaderRenderer(props, 4),
|
||||
h5: (props) => HeaderRenderer(props, 5),
|
||||
h6: (props) => HeaderRenderer(props, 6),
|
||||
code: (props) => CodeRenderer(props),
|
||||
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex, this.curUuid),
|
||||
a: Link,
|
||||
h1: (props) => <Header {...props} hnum={1} />,
|
||||
h2: (props) => <Header {...props} hnum={2} />,
|
||||
h3: (props) => <Header {...props} hnum={3} />,
|
||||
h4: (props) => <Header {...props} hnum={4} />,
|
||||
h5: (props) => <Header {...props} hnum={5} />,
|
||||
h6: (props) => <Header {...props} hnum={6} />,
|
||||
code: Code,
|
||||
pre: (props) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("markdown content", this.props.extraClassName)} style={this.props.style}>
|
||||
<div className={clsx("markdown content", className)} style={this.props.style}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
|
@ -1,112 +0,0 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CopyButton } from "@/elements";
|
||||
import { clsx } from "clsx";
|
||||
import * as mobx from "mobx";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
|
||||
import "./markdown.less";
|
||||
|
||||
function Link(props: any): JSX.Element {
|
||||
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
||||
return (
|
||||
<a href={newUrl} target="_blank" rel={"noopener"}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function Header(props: any, hnum: number): JSX.Element {
|
||||
return <div className={clsx("title", "is-" + hnum)}>{props.children}</div>;
|
||||
}
|
||||
|
||||
function Code(props: any): JSX.Element {
|
||||
return <code>{props.children}</code>;
|
||||
}
|
||||
|
||||
const CodeBlock = mobxReact.observer(
|
||||
(props: { children: React.ReactNode; onClickExecute?: (cmd: string) => void }): JSX.Element => {
|
||||
const copied: OV<boolean> = mobx.observable.box(false, { name: "copied" });
|
||||
|
||||
const getTextContent = (children: any) => {
|
||||
if (typeof children === "string") {
|
||||
return children;
|
||||
} else if (Array.isArray(children)) {
|
||||
return children.map(getTextContent).join("");
|
||||
} else if (children.props && children.props.children) {
|
||||
return getTextContent(children.props.children);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
let textToCopy = getTextContent(props.children);
|
||||
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
copied.set(true);
|
||||
setTimeout(() => copied.set(false), 2000); // Reset copied state after 2 seconds
|
||||
};
|
||||
|
||||
const handleExecute = (e: React.MouseEvent) => {
|
||||
let textToCopy = getTextContent(props.children);
|
||||
textToCopy = textToCopy.replace(/\n$/, ""); // remove trailing newline
|
||||
if (props.onClickExecute) {
|
||||
props.onClickExecute(textToCopy);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<pre className="codeblock">
|
||||
{props.children}
|
||||
<div className="codeblock-actions">
|
||||
<CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
|
||||
<If condition={props.onClickExecute}>
|
||||
<i className="fa-regular fa-square-terminal" onClick={handleExecute}></i>
|
||||
</If>
|
||||
</div>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@mobxReact.observer
|
||||
class Markdown2 extends React.Component<
|
||||
{
|
||||
text: string;
|
||||
style?: any;
|
||||
className?: string;
|
||||
onClickExecute?: (cmd: string) => void;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
render() {
|
||||
let { text, className, onClickExecute } = this.props;
|
||||
let markdownComponents = {
|
||||
a: Link,
|
||||
h1: (props) => <Header {...props} hnum={1} />,
|
||||
h2: (props) => <Header {...props} hnum={2} />,
|
||||
h3: (props) => <Header {...props} hnum={3} />,
|
||||
h4: (props) => <Header {...props} hnum={4} />,
|
||||
h5: (props) => <Header {...props} hnum={5} />,
|
||||
h6: (props) => <Header {...props} hnum={6} />,
|
||||
code: Code,
|
||||
pre: (props) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("markdown content", className)} style={this.props.style}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Markdown2 };
|
@ -178,7 +178,7 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
|
||||
<div className="line-actions">
|
||||
<Choose>
|
||||
<When condition={containerType == appconst.LineContainer_Main}>
|
||||
<div key="chat" title="Restart Command" className="line-icon" onClick={this.clickChat}>
|
||||
<div key="chat" title="Ask Wave AI" className="line-icon" onClick={this.clickChat}>
|
||||
<i className="fa-sharp fa-regular fa-sparkles fa-fw" />
|
||||
</div>
|
||||
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
|
||||
|
@ -7,7 +7,7 @@ import * as mobx from "mobx";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { For, If } from "tsx-control-statements/components";
|
||||
import { Markdown2, TypingIndicator } from "@/elements";
|
||||
import { Markdown, TypingIndicator } from "@/elements";
|
||||
import type { OverlayScrollbars } from "overlayscrollbars";
|
||||
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||
import tinycolor from "tinycolor2";
|
||||
@ -40,9 +40,6 @@ class ChatKeyBindings extends React.Component<{ component: ChatSidebar }, {}> {
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
|
||||
return component.onArrowDownPressed();
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "aichat:setCmdInputValue", (waveEvent) => {
|
||||
return component.onSetCmdInputValue();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
@ -75,7 +72,7 @@ class ChatItem extends React.Component<
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-user"></i>
|
||||
</div>
|
||||
<Markdown2 className="msg-text" text={chatItem.userquery} />
|
||||
<Markdown className="msg-text" text={chatItem.userquery} />
|
||||
</>
|
||||
);
|
||||
if (isassistantresponse) {
|
||||
@ -97,7 +94,7 @@ class ChatItem extends React.Component<
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-sparkles"></i>
|
||||
</div>
|
||||
<Markdown2 text={assistantresponse.message} onClickExecute={onSetCmdInputValue} />
|
||||
<Markdown text={assistantresponse.message} onClickExecute={onSetCmdInputValue} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -195,7 +192,6 @@ class ChatSidebar extends React.Component<{}, {}> {
|
||||
value: OV<string> = mobx.observable.box("", { deep: false, name: "value" });
|
||||
osInstance: OverlayScrollbars;
|
||||
termFontSize: number = 14;
|
||||
blockIndex: number;
|
||||
disposeReaction: () => void;
|
||||
|
||||
constructor(props) {
|
||||
@ -227,32 +223,21 @@ class ChatSidebar extends React.Component<{}, {}> {
|
||||
}
|
||||
);
|
||||
if (this.sidebarRef.current) {
|
||||
this.sidebarRef.current.addEventListener("click", this.handleSidebarClick);
|
||||
this.sidebarRef.current.addEventListener("click", this.onSidebarClick);
|
||||
}
|
||||
document.addEventListener("click", this.handleClickOutside);
|
||||
this.requestChatUpdate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.sidebarRef.current) {
|
||||
this.sidebarRef.current.removeEventListener("click", this.handleSidebarClick);
|
||||
this.sidebarRef.current.removeEventListener("click", this.onSidebarClick);
|
||||
}
|
||||
document.removeEventListener("click", this.handleClickOutside);
|
||||
GlobalModel.sidebarchatModel.resetFocus();
|
||||
if (this.disposeReaction) {
|
||||
this.disposeReaction();
|
||||
}
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
handleClickOutside(e: MouseEvent) {
|
||||
const sidebar = this.sidebarRef.current;
|
||||
if (sidebar && !sidebar.contains(e.target as Node)) {
|
||||
GlobalModel.sidebarchatModel.resetFocus();
|
||||
GlobalModel.inputModel.giveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
requestChatUpdate() {
|
||||
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
|
||||
if (chatMessageItems == null || chatMessageItems.length === 0) {
|
||||
@ -309,10 +294,16 @@ class ChatSidebar extends React.Component<{}, {}> {
|
||||
|
||||
@mobx.action.bound
|
||||
onEnterKeyPressed() {
|
||||
const blockIndex = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
|
||||
if (blockIndex != null) {
|
||||
this.onSetCmdInputValue();
|
||||
return true;
|
||||
}
|
||||
const messageStr = this.value.get();
|
||||
this.submitChatMessage(messageStr);
|
||||
this.value.set("");
|
||||
GlobalModel.sidebarchatModel.resetCmdAndOutput();
|
||||
return true;
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
@ -324,6 +315,11 @@ class ChatSidebar extends React.Component<{}, {}> {
|
||||
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
onBlur() {
|
||||
GlobalModel.sidebarchatModel.resetFocus();
|
||||
}
|
||||
|
||||
updatePreTagOutline(clickedPre?) {
|
||||
const pres = this.chatWindowRef.current?.querySelectorAll("pre");
|
||||
if (pres == null) {
|
||||
@ -340,7 +336,7 @@ class ChatSidebar extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
handleSidebarClick(event) {
|
||||
onSidebarClick(event) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target.closest(".copy-button") ||
|
||||
@ -526,7 +522,6 @@ class ChatSidebar extends React.Component<{}, {}> {
|
||||
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
|
||||
const renderAIChatKeybindings = GlobalModel.sidebarchatModel.hasFocus();
|
||||
const textAreaValue = this.value.get();
|
||||
|
||||
return (
|
||||
<div ref={this.sidebarRef} className="sidebarchat">
|
||||
<If condition={renderAIChatKeybindings}>
|
||||
@ -546,6 +541,7 @@ class ChatSidebar extends React.Component<{}, {}> {
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="sidebarchat-input chat-textarea"
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onTextAreaFocus}
|
||||
onMouseDown={this.onTextAreaMouseDown} // When the user clicks on the textarea
|
||||
onChange={this.onTextAreaChange}
|
||||
|
@ -1,72 +0,0 @@
|
||||
.cmd-aichat {
|
||||
padding-bottom: 0 !important;
|
||||
|
||||
.chat-window {
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 5px;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
// This is the filler that will push the chat messages to the bottom until the chat window is full
|
||||
.filler {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.chat-textarea {
|
||||
color: var(--app-text-primary-color);
|
||||
background-color: var(--cmdinput-textarea-bg);
|
||||
resize: none;
|
||||
width: 100%;
|
||||
border: transparent;
|
||||
outline: none;
|
||||
overflow: auto;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--termfontfamily);
|
||||
font-weight: normal;
|
||||
line-height: var(--termlineheight);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
margin-top: calc(var(--termpad) * 2);
|
||||
margin-bottom: calc(var(--termpad) * 2);
|
||||
|
||||
.chat-msg-header {
|
||||
display: flex;
|
||||
margin-bottom: 2px;
|
||||
|
||||
i {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.chat-username {
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg-assistant {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
|
||||
.chat-msg-user {
|
||||
.msg-text {
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-msg-error {
|
||||
color: var(--cmdinput-text-error);
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import { Markdown } from "@/elements";
|
||||
import { AuxiliaryCmdView } from "./auxview";
|
||||
import * as appconst from "@/app/appconst";
|
||||
|
||||
import "./aichat.less";
|
||||
|
||||
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
|
||||
componentDidMount(): void {
|
||||
const AIChatObject = this.props.AIChatObject;
|
||||
const keybindManager = GlobalModel.keybindManager;
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
|
||||
AIChatObject.onEnterKeyPressed();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:expandTextInput", (waveEvent) => {
|
||||
AIChatObject.onExpandInputPressed();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
|
||||
inputModel.closeAuxView();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => {
|
||||
inputModel.clearAIAssistantChat();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
|
||||
return AIChatObject.onArrowUpPressed();
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
|
||||
return AIChatObject.onArrowDownPressed();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
GlobalModel.keybindManager.unregisterDomain("aichat");
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class AIChat extends React.Component<{}, {}> {
|
||||
chatListKeyCount: number = 0;
|
||||
chatWindowScrollRef: React.RefObject<HTMLDivElement>;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
termFontSize: number = 14;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
mobx.makeObservable(this);
|
||||
this.chatWindowScrollRef = React.createRef();
|
||||
this.textAreaRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
if (this.chatWindowScrollRef?.current != null) {
|
||||
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
|
||||
}
|
||||
if (this.textAreaRef.current != null) {
|
||||
this.textAreaRef.current.focus();
|
||||
inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef);
|
||||
}
|
||||
this.requestChatUpdate();
|
||||
this.onTextAreaChange(null);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.chatWindowScrollRef?.current != null) {
|
||||
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
requestChatUpdate() {
|
||||
this.submitChatMessage("");
|
||||
}
|
||||
|
||||
submitChatMessage(messageStr: string) {
|
||||
const curLine = GlobalModel.inputModel.curLine;
|
||||
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
|
||||
prtn.then((rtn) => {
|
||||
if (!rtn.success) {
|
||||
console.log("submit chat command error: " + rtn.error);
|
||||
}
|
||||
}).catch((_) => {});
|
||||
}
|
||||
|
||||
getLinePos(elem: any): { numLines: number; linePos: number } {
|
||||
const numLines = elem.value.split("\n").length;
|
||||
const linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
|
||||
return { numLines, linePos };
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
onTextAreaFocused(e: any) {
|
||||
GlobalModel.inputModel.setAuxViewFocus(true);
|
||||
this.onTextAreaChange(e);
|
||||
}
|
||||
|
||||
@mobx.action.bound
|
||||
onTextAreaBlur(e: any) {
|
||||
//GlobalModel.inputModel.setAuxViewFocus(false);
|
||||
}
|
||||
|
||||
// Adjust the height of the textarea to fit the text
|
||||
@boundMethod
|
||||
onTextAreaChange(e: any) {
|
||||
// Calculate the bounding height of the text area
|
||||
const textAreaMaxLines = 4;
|
||||
const textAreaLineHeight = this.termFontSize * 1.5;
|
||||
const textAreaMinHeight = textAreaLineHeight;
|
||||
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
|
||||
|
||||
// Get the height of the wrapped text area content. Courtesy of https://stackoverflow.com/questions/995168/textarea-to-resize-based-on-content-length
|
||||
this.textAreaRef.current.style.height = "1px";
|
||||
const scrollHeight: number = this.textAreaRef.current.scrollHeight;
|
||||
|
||||
// Set the new height of the text area, bounded by the min and max height.
|
||||
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
||||
this.textAreaRef.current.style.height = newHeight + "px";
|
||||
}
|
||||
|
||||
onTextAreaInput(e: any) {
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
}
|
||||
|
||||
onEnterKeyPressed() {
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
const currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return;
|
||||
}
|
||||
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
||||
const messageStr = currentRef.value;
|
||||
this.submitChatMessage(messageStr);
|
||||
currentRef.value = "";
|
||||
} else {
|
||||
mobx.action(() => {
|
||||
inputModel.grabCodeSelectSelection();
|
||||
inputModel.setAuxViewFocus(false);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
onExpandInputPressed() {
|
||||
const currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return;
|
||||
}
|
||||
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
}
|
||||
|
||||
onArrowUpPressed(): boolean {
|
||||
const currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return false;
|
||||
}
|
||||
if (this.getLinePos(currentRef).linePos > 1) {
|
||||
// normal up arrow
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
return false;
|
||||
}
|
||||
GlobalModel.inputModel.codeSelectSelectNextOldestCodeBlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
onArrowDownPressed(): boolean {
|
||||
const currentRef = this.textAreaRef.current;
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
if (currentRef == null) {
|
||||
return false;
|
||||
}
|
||||
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
return false;
|
||||
}
|
||||
inputModel.codeSelectSelectNextNewestCodeBlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onKeyDown(e: any) {}
|
||||
|
||||
renderError(err: string): any {
|
||||
return <div className="chat-msg-error">{err}</div>;
|
||||
}
|
||||
|
||||
renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any {
|
||||
const curKey = "chatmsg-" + this.chatListKeyCount;
|
||||
this.chatListKeyCount++;
|
||||
const senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
|
||||
const msgClassName = "chat-msg " + senderClassName;
|
||||
let innerHTML: React.JSX.Element = (
|
||||
<span>
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-user"></i>
|
||||
<div className="chat-username">You</div>
|
||||
</div>
|
||||
<p className="msg-text">{chatItem.userquery}</p>
|
||||
</span>
|
||||
);
|
||||
if (chatItem.isassistantresponse) {
|
||||
if (chatItem.assistantresponse.error != null && chatItem.assistantresponse.error != "") {
|
||||
innerHTML = this.renderError(chatItem.assistantresponse.error);
|
||||
} else {
|
||||
innerHTML = (
|
||||
<span>
|
||||
<div className="chat-msg-header">
|
||||
<i className="fa-sharp fa-solid fa-sparkles"></i>
|
||||
<div className="chat-username">AI Assistant</div>
|
||||
</div>
|
||||
<Markdown text={chatItem.assistantresponse.message} codeSelect />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={msgClassName} key={curKey}>
|
||||
{innerHTML}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
|
||||
const chitem: OpenAICmdInfoChatMessageType = null;
|
||||
const renderKeybindings = GlobalModel.inputModel.shouldRenderAuxViewKeybindings(appconst.InputAuxView_AIChat);
|
||||
return (
|
||||
<AuxiliaryCmdView
|
||||
title="Wave AI"
|
||||
className="cmd-aichat"
|
||||
onClose={() => GlobalModel.inputModel.closeAuxView()}
|
||||
iconClass="fa-sharp fa-solid fa-sparkles"
|
||||
>
|
||||
<If condition={renderKeybindings}>
|
||||
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
|
||||
</If>
|
||||
<div className="chat-window" ref={this.chatWindowScrollRef}>
|
||||
<div className="filler"></div>
|
||||
<For each="chitem" index="idx" of={chatMessageItems}>
|
||||
{this.renderChatMessage(chitem)}
|
||||
</For>
|
||||
</div>
|
||||
<div className="chat-input">
|
||||
<textarea
|
||||
key="main"
|
||||
ref={this.textAreaRef}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
id="chat-cmd-input"
|
||||
onFocus={this.onTextAreaFocused}
|
||||
onBlur={this.onTextAreaBlur}
|
||||
onChange={this.onTextAreaChange}
|
||||
onInput={this.onTextAreaInput}
|
||||
onKeyDown={this.onKeyDown}
|
||||
style={{ fontSize: this.termFontSize }}
|
||||
className="chat-textarea"
|
||||
placeholder="Send a Message..."
|
||||
></textarea>
|
||||
</div>
|
||||
</AuxiliaryCmdView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { AIChat };
|
@ -131,7 +131,7 @@
|
||||
}
|
||||
|
||||
.textarea-ghost {
|
||||
color: var(--app-text-secondary-color);
|
||||
color: var(--cmdinput-ghost-text-color);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ import { InfoMsg } from "./infomsg";
|
||||
import { HistoryInfo } from "./historyinfo";
|
||||
import { Prompt } from "@/common/prompt/prompt";
|
||||
import { CenteredIcon, RotateIcon } from "@/common/icons/icons";
|
||||
import { AIChat } from "./aichat";
|
||||
import * as util from "@/util/util";
|
||||
import * as appconst from "@/app/appconst";
|
||||
import { AutocompleteSuggestionView } from "./suggestionview";
|
||||
@ -29,6 +28,7 @@ dayjs.extend(localizedFormat);
|
||||
class CmdInput extends React.Component<{}, {}> {
|
||||
cmdInputRef: React.RefObject<any> = React.createRef();
|
||||
promptRef: React.RefObject<any> = React.createRef();
|
||||
sbcTimeoutId: NodeJS.Timeout = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -57,6 +57,13 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
this.updateCmdInputHeight();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.sbcTimeoutId) {
|
||||
clearTimeout(this.sbcTimeoutId);
|
||||
this.sbcTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleInnerHeightUpdate(): void {
|
||||
this.updateCmdInputHeight();
|
||||
@ -96,9 +103,15 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
|
||||
@mobx.action.bound
|
||||
clickAIChatAction(e: any): void {
|
||||
const rightSidebarModel = GlobalModel.rightSidebarModel;
|
||||
const width = rightSidebarModel.getWidth(true);
|
||||
rightSidebarModel.saveState(width, false);
|
||||
const isCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
|
||||
GlobalModel.rightSidebarModel.setCollapsed(!isCollapsed);
|
||||
if (isCollapsed) {
|
||||
this.sbcTimeoutId = setTimeout(() => {
|
||||
GlobalModel.inputModel.setChatSidebarFocus();
|
||||
}, 100);
|
||||
} else {
|
||||
GlobalModel.inputModel.setChatSidebarFocus(false);
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@ -187,10 +200,6 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
<div className="cmd-input-grow-spacer"></div>
|
||||
<HistoryInfo />
|
||||
</When>
|
||||
<When condition={openView === appconst.InputAuxView_AIChat}>
|
||||
<div className="cmd-input-grow-spacer"></div>
|
||||
<AIChat />
|
||||
</When>
|
||||
<When condition={openView === appconst.InputAuxView_Info}>
|
||||
<InfoMsg key="infomsg" />
|
||||
</When>
|
||||
|
@ -187,9 +187,7 @@ export class AutocompleteModel {
|
||||
* @see getPrimarySuggestionIndex
|
||||
*/
|
||||
getPrimarySuggestionCompletion(): string {
|
||||
if (!this.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
if (!this.isEnabled || !this.globalModel.inputModel.curLine) return null;
|
||||
const suggestionIndex = this.getPrimarySuggestionIndex();
|
||||
const retVal = this.getSuggestionCompletion(suggestionIndex);
|
||||
if (retVal) {
|
||||
|
@ -234,10 +234,21 @@ class Screen {
|
||||
}
|
||||
|
||||
refocusLine(sdata: ScreenDataType, oldFocusType: string, oldSelectedLine: number): void {
|
||||
if (this.globalModel.activeMainView.get() != "session") {
|
||||
return;
|
||||
}
|
||||
let isCmdFocus = sdata.focustype == "cmd";
|
||||
if (!isCmdFocus) {
|
||||
return;
|
||||
}
|
||||
if (document.activeElement != null) {
|
||||
if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.globalModel.modalsModel.hasOpenModals()) {
|
||||
return;
|
||||
}
|
||||
let curLineFocus = this.globalModel.getFocusedLine();
|
||||
let sline: LineType = null;
|
||||
if (sdata.selectedline != 0) {
|
||||
|
@ -6,7 +6,6 @@ import * as mobx from "mobx";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { sprintf } from "sprintf-js";
|
||||
import { Markdown } from "@/elements";
|
||||
import { GlobalModel } from "@/models/global";
|
||||
|
||||
import "./markdown.less";
|
||||
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf8"
|
||||
@ -673,3 +674,26 @@ func GetFirstLine(s string) string {
|
||||
}
|
||||
return s[0:idx]
|
||||
}
|
||||
|
||||
func TrimQuotes(s string) (string, bool) {
|
||||
if len(s) > 2 && s[0] == '"' {
|
||||
trimmed, err := strconv.Unquote(s)
|
||||
if err != nil {
|
||||
return s, false
|
||||
}
|
||||
return trimmed, true
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
||||
func TryTrimQuotes(s string) string {
|
||||
trimmed, _ := TrimQuotes(s)
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func ReplaceQuotes(s string, shouldReplace bool) string {
|
||||
if shouldReplace {
|
||||
return strconv.Quote(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ var SetVarScopes = []SetVarScope{
|
||||
{ScopeName: "remote", VarNames: []string{}},
|
||||
}
|
||||
|
||||
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
|
||||
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-zA-Z0-9][a-zA-Z0-9._@:\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
|
||||
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
|
||||
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
|
||||
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
|
||||
@ -769,7 +769,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
|
||||
} else {
|
||||
return nil, fmt.Errorf("error in Eval Meta Command: %w", rtnErr)
|
||||
}
|
||||
if !resolveBool(pk.Kwargs[KwArgNoHist], false) {
|
||||
if !resolveBool(pk.Kwargs[KwArgNoHist], false) && pk.EphemeralOpts == nil {
|
||||
// TODO should this be "pk" or "newPk" (2nd arg)
|
||||
err := addToHistory(ctx, pk, historyContext, (newPk.MetaCmd != "run"), (rtnErr != nil))
|
||||
if err != nil {
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/kevinburke/ssh_config"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
|
||||
@ -110,10 +111,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
}
|
||||
|
||||
unencryptedPrivateKey, err := ssh.ParseRawPrivateKey(privateKey)
|
||||
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
if err == nil {
|
||||
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
|
||||
if err == nil {
|
||||
@ -124,7 +121,10 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
}
|
||||
return []ssh.Signer{signer}, err
|
||||
}
|
||||
|
||||
}
|
||||
if _, ok := err.(*ssh.PassphraseMissingError); !ok {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||
@ -137,10 +137,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
}
|
||||
|
||||
unencryptedPrivateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(privateKey, []byte(passphrase))
|
||||
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
if err == nil {
|
||||
signer, err := ssh.NewSignerFromKey(unencryptedPrivateKey)
|
||||
if err == nil {
|
||||
@ -152,18 +148,10 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
return []ssh.Signer{signer}, err
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(passphrase))
|
||||
if err == nil {
|
||||
log.Printf("with passphrase %v\n", signer.PublicKey().Marshal())
|
||||
return []ssh.Signer{signer}, err
|
||||
}
|
||||
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
*/
|
||||
if err != x509.IncorrectPasswordError && err.Error() != "bcrypt_pbkdf: empty password" {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
|
||||
// batch mode deactivates user input
|
||||
if sshKeywords.BatchMode {
|
||||
@ -201,16 +189,6 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords,
|
||||
})
|
||||
}
|
||||
return []ssh.Signer{signer}, err
|
||||
|
||||
/*
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(response.Text))
|
||||
if err != nil {
|
||||
// skip this key and try with the next
|
||||
return createDummySigner()
|
||||
}
|
||||
log.Printf("with passphrase %v\n", signer.PublicKey().Marshal())
|
||||
return []ssh.Signer{signer}, err
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -809,9 +787,9 @@ func findSshConfigKeywords(hostPattern string, sshAuthSock string) (*SshKeywords
|
||||
return nil, err
|
||||
}
|
||||
if identityAgentRaw == "" {
|
||||
sshKeywords.IdentityAgent = sshAuthSock
|
||||
sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(strings.TrimSpace(string(sshAuthSock))))
|
||||
} else {
|
||||
sshKeywords.IdentityAgent = identityAgentRaw
|
||||
sshKeywords.IdentityAgent = base.ExpandHomeDir(utilfn.TryTrimQuotes(identityAgentRaw))
|
||||
}
|
||||
|
||||
return sshKeywords, nil
|
||||
|
Loading…
Reference in New Issue
Block a user